Merge branch 'master' into master-ios

This commit is contained in:
spaced4ndy
2023-05-16 15:06:01 +04:00
30 changed files with 505 additions and 161 deletions
@@ -6,6 +6,8 @@ import SectionDividerSpaced
import SectionSpacer
import SectionTextFooter
import SectionView
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -21,13 +23,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import kotlinx.datetime.Clock
@@ -68,6 +70,15 @@ fun GroupMemberInfoView(
}
}
},
connectViaAddress = { connReqUri ->
val uri = Uri.parse(connReqUri)
withUriAction(uri) { linkType ->
withApi {
Log.d(TAG, "connectViaUri: connecting")
connectViaUri(chatModel, linkType, uri)
}
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
@@ -147,11 +158,21 @@ fun GroupMemberInfoLayout(
connectionCode: String?,
getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit,
connectViaAddress: (String) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
fun knownDirectChat(contactId: Long): Chat? {
val chat = getContactChat(contactId)
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
chat
} else {
null
}
}
Column(
Modifier
.fillMaxWidth()
@@ -165,12 +186,12 @@ fun GroupMemberInfoLayout(
}
SectionSpacer()
val contactId = member.memberContactId
if (member.memberActive) {
val contactId = member.memberContactId
if (contactId != null) {
SectionView {
val chat = getContactChat(contactId)
if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { openDirectChat(contactId) })
}
if (connectionCode != null) {
@@ -186,6 +207,13 @@ fun GroupMemberInfoLayout(
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(context, member.contactLink) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
} else {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(member.displayName))
}
SectionDividerSpaced()
@@ -295,6 +323,17 @@ fun OpenChatButton(onClick: () -> Unit) {
)
}
@Composable
fun ConnectViaAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_link),
stringResource(R.string.connect_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
)
}
@Composable
private fun RoleSelectionRow(
roles: List<GroupMemberRole>,
@@ -354,6 +393,7 @@ fun PreviewGroupMemberInfoLayout() {
connectionCode = "123",
getContactChat = { Chat.sampleData },
openDirectChat = {},
connectViaAddress = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
@@ -128,7 +128,7 @@
<string name="error_sending_message">Chyba odesílání zprávy</string>
<string name="error_adding_members">Chyba přidávání členů</string>
<string name="contact_already_exists">Kontakt již existuje</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="invalid_connection_link">Neplatný odkaz na spojení</string>
<string name="error_accepting_contact_request">Chyba příjmu požadavku od kontaktu</string>
<string name="error_changing_address">Chyba změny adresy</string>
@@ -63,7 +63,7 @@
<string name="error_receiving_file">Fehler beim Empfangen der Datei</string>
<string name="error_creating_address">Fehler beim Erstellen der Adresse</string>
<string name="contact_already_exists">Kontakt ist bereits vorhanden</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s</xliff:g> verbunden.</string>
<string name="invalid_connection_link">Ungültiger Verbindungslink</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string>
<string name="connection_error_auth">Verbindungsfehler (AUTH)</string>
@@ -867,7 +867,7 @@
<string name="voice_messages">Mensajes de voz</string>
<string name="voice_messages_are_prohibited">Los mensajes de voz no están permitidos en este grupo.</string>
<string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string>
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a <xliff:g id="contactName" example="Alice">%1$s! </xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="welcome">¡Bienvenido!</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil Chat será enviado
\na tu contacto</string>
@@ -60,7 +60,7 @@
<string name="invalid_connection_link">Lien de connection invalide</string>
<string name="connection_timeout">Délai de connexion</string>
<string name="error_sending_message">Erreur lors de l\'envoi du message</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="connection_error_auth">Erreur de connexion (AUTH)</string>
<string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler.
\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string>
@@ -207,7 +207,7 @@
<string name="simplex_link_mode_description">Descrizione</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode_browser_warning">Aprire il link nel browser può ridurre la privacy e la sicurezza della connessione. I link SimpleX non fidati saranno in rosso.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.
\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string>
<string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string>
@@ -927,7 +927,7 @@
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g>ロゴ</string>
<string name="you_need_to_allow_to_send_voice">音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。</string>
<string name="you_must_use_the_most_recent_version_of_database">あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。</string>
<string name="you_are_already_connected_to_vName_via_this_link">既に繋がってます: <xliff:g id="contactName" example="Alice">%1$s!</xliff:g></string>
<string name="you_are_already_connected_to_vName_via_this_link">既に繋がってます: <xliff:g id="contactName" example="Alice">%1$s</xliff:g></string>
<string name="alert_title_cant_invite_contacts_descr">シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません。</string>
<string name="description_you_shared_one_time_link">使い捨てリンクを送りました</string>
<string name="profile_will_be_sent_to_contact_sending_link">リンクを送ってくれた連絡先にあなたのプロフィールを送ります。</string>
@@ -638,7 +638,7 @@
<string name="group_member_status_removed">verwijderd</string>
<string name="group_main_profile_sent">Uw chat profiel wordt verzonden naar de groepsleden</string>
<string name="failed_to_create_user_duplicate_desc">Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam.</string>
<string name="you_are_already_connected_to_vName_via_this_link">U bent al verbonden met <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">U bent al verbonden met <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="error_smp_test_failed_at_step">Test mislukt bij stap %s.</string>
<string name="smp_server_test_secure_queue">Veilige wachtrij</string>
<string name="database_initialization_error_desc">De database werkt niet goed. Tik voor meer informatie</string>
@@ -1028,7 +1028,7 @@
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> CZŁONKÓW</string>
<string name="simplex_service_notification_title">serwis <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> chce się z Tobą połączyć przez</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jesteś już połączony z <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jesteś już połączony z <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="you_can_connect_to_simplex_chat_founder">Możesz <font color="#0088ff">połączyć się z deweloperami <xliff:g id="appNameFull">SimpleX Chat</xliff:g> aby zadać wszelkie pytania i otrzymywać aktualizacje</font>.</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Możesz udostępnić link lub kod QR - każdy będzie mógł dołączyć do grupy. Nie stracisz członków grupy, jeśli później ją usuniesz.</string>
<string name="you_must_use_the_most_recent_version_of_database">Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów.</string>
@@ -862,7 +862,7 @@
<string name="video_call_no_encryption">chamada de vídeo (não criptografada ponta-a-ponta)</string>
<string name="stop_chat_question">Parar chat\?</string>
<string name="simplex_link_mode_browser">Pelo navegador</string>
<string name="you_are_already_connected_to_vName_via_this_link">Você já está conectado a<xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Você já está conectado a<xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="database_initialization_error_desc">O banco de dados não está funcionando corretamente. Toque para saber mais</string>
<string name="waiting_for_image">Aguardando imagem</string>
<string name="show_QR_code">Mostrar código QR</string>
@@ -63,7 +63,7 @@
<string name="error_receiving_file">Ошибка при получении файла</string>
<string name="error_creating_address">Ошибка при создании адреса</string>
<string name="contact_already_exists">Существующий контакт</string>
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с контактом <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с контактом <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="invalid_connection_link">Ошибка в ссылке контакта</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Пожалуйста, проверьте, что Вы использовали правильную ссылку, или попросите Ваш контакт отправить Вам новую.</string>
<string name="connection_error_auth">Ошибка соединения (AUTH)</string>
@@ -589,7 +589,7 @@
<string name="icon_descr_call_pending_sent">待定来电</string>
<string name="connection_error_auth_desc">除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。
\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。</string>
<string name="you_are_already_connected_to_vName_via_this_link">您已经连接到 <xliff:g id="contactName" example="Alice">%1$s!</xliff:g></string>
<string name="you_are_already_connected_to_vName_via_this_link">您已经连接到 <xliff:g id="contactName" example="Alice">%1$s</xliff:g></string>
<string name="your_chat_profile_will_be_sent_to_your_contact">您的聊天资料将被发送
\n给您的联系人</string>
<string name="users_delete_with_connections">资料和服务器连接</string>
@@ -228,7 +228,7 @@
<string name="simplex_link_mode_browser">透過瀏覽器</string>
<string name="error_sending_message">傳送訊息時出錯</string>
<string name="contact_already_exists">聯絡人已存在</string>
<string name="you_are_already_connected_to_vName_via_this_link">你已經連接到 <xliff:g id="contactName" example="Alice">%1$s!</xliff:g></string>
<string name="you_are_already_connected_to_vName_via_this_link">你已經連接到 <xliff:g id="contactName" example="Alice">%1$s</xliff:g></string>
<string name="v4_5_transport_isolation_descr">使用個人檔案(預設值)或使用連接(測試版)。</string>
<string name="v4_5_reduced_battery_usage">減少電量使用</string>
<string name="v4_5_reduced_battery_usage_descr">更多改進即將推出!</string>
@@ -90,7 +90,7 @@
<string name="error_receiving_file">Error receiving file</string>
<string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="invalid_connection_link">Invalid connection link</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="connection_error_auth">Connection error (AUTH)</string>
@@ -1,20 +1,11 @@
//import UIKit
import SimpleXChat
let s = """
{
"contactConnection" : {
"contactConnection" : {
"viaContactUri" : false,
"pccConnId" : 456,
"pccAgentConnId" : "cTdjbmR4ZzVzSmhEZHdzMQ==",
"pccConnStatus" : "new",
"updatedAt" : "2022-04-24T11:59:23.703162Z",
"createdAt" : "2022-04-24T11:59:23.703162Z"
}
}
}
let s =
"""
{}
"""
//let s = "\"2022-04-24T11:59:23.703162Z\""
//let json = getJSONDecoder()
//let d = s.data(using: .utf8)!
//print (try! json.decode(ChatInfo.self, from: d))
let json = getJSONDecoder()
let d = s.data(using: .utf8)!
print (try! json.decode(APIResponse.self, from: d))
+30 -14
View File
@@ -238,16 +238,9 @@ final class ChatModel: ObservableObject {
}
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
let ci = reversedChatItems[i]
if let i = getChatItemIndex(cItem) {
withAnimation {
self.reversedChatItems[i] = cItem
self.reversedChatItems[i].viewTimestamp = .now
// on some occasions the confirmation of message being accepted by the server (tick)
// arrives earlier than the response from API, and item remains without tick
if case .sndNew = cItem.meta.itemStatus {
self.reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
}
_updateChatItem(at: i, with: cItem)
}
return false
} else {
@@ -264,7 +257,30 @@ final class ChatModel: ObservableObject {
}
}
}
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
withAnimation {
_updateChatItem(at: i, with: cItem)
}
}
}
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
let ci = reversedChatItems[i]
reversedChatItems[i] = cItem
reversedChatItems[i].viewTimestamp = .now
// on some occasions the confirmation of message being accepted by the server (tick)
// arrives earlier than the response from API, and item remains without tick
if case .sndNew = cItem.meta.itemStatus {
reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
}
}
private func getChatItemIndex(_ cItem: ChatItem) -> Int? {
reversedChatItems.firstIndex(where: { $0.id == cItem.id })
}
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if cItem.isRcvNew {
decreaseUnreadCounter(cInfo)
@@ -277,7 +293,7 @@ final class ChatModel: ObservableObject {
}
// remove from current chat
if chatId == cInfo.id {
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if let i = getChatItemIndex(cItem) {
_ = withAnimation {
self.reversedChatItems.remove(at: i)
}
@@ -357,7 +373,7 @@ final class ChatModel: ObservableObject {
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
markCurrentChatRead(fromIndex: i)
_updateChat(cInfo.id) { chat in
var unreadBelow = 0
@@ -405,7 +421,7 @@ final class ChatModel: ObservableObject {
// update preview
decreaseUnreadCounter(cInfo)
// update current chat
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
markChatItemRead_(i)
}
}
@@ -450,7 +466,7 @@ final class ChatModel: ObservableObject {
}
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
if let i = reversedChatItems.firstIndex(where: { $0.id == ci.id }), i < reversedChatItems.count - 1 {
if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 {
return reversedChatItems[i + 1]
} else {
return nil
+52 -27
View File
@@ -314,7 +314,9 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
chatModel.messageDelivery[cItem.id] = endTask
return cItem
}
if !networkErrorAlert(r) {
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
} else {
sendMessageErrorAlert(r)
}
endTask()
@@ -343,6 +345,12 @@ func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent
throw r
}
func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, reaction: MsgReaction, add: Bool) async throws -> ChatItem {
let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, reaction: reaction, add: add), bgDelay: msgDelay)
if case let .chatItemReaction(_, reaction, _) = r { return reaction.chatReaction.chatItem }
throw r
}
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
@@ -510,59 +518,71 @@ func apiAddContact() async -> String? {
}
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
connectionErrorAlert(r)
AlertManager.shared.showAlert(connectionErrorAlert(r))
return nil
}
func apiConnect(connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(connReq: connReq)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return nil
} else {
return connReqType
}
}
func apiConnect_(connReq: String) async -> (ConnReqType?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return nil
return (nil, nil)
}
let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq))
let am = AlertManager.shared
switch r {
case .sentConfirmation: return .invitation
case .sentInvitation: return .contact
case .sentConfirmation: return (.invitation, nil)
case .sentInvitation: return (.contact, nil)
case let .contactAlreadyExists(_, contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
}
am.showAlertMsg(
let alert = mkAlert(
title: "Contact already exists",
message: "You are already connected to \(contact.displayName)."
)
return nil
return (nil, alert)
case .chatCmdError(_, .error(.invalidConnReq)):
am.showAlertMsg(
let alert = mkAlert(
title: "Invalid connection link",
message: "Please check that you used the correct link or ask your contact to send you another one."
)
return nil
return (nil, alert)
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))):
am.showAlertMsg(
let alert = mkAlert(
title: "Connection error (AUTH)",
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
return nil
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" {
am.showAlertMsg(
let alert = mkAlert(
title: "Already connected?",
message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(responseError(r)))."
)
return nil
return (nil, alert)
}
default: ()
}
connectionErrorAlert(r)
return nil
let alert = connectionErrorAlert(r)
return (nil, alert)
}
private func connectionErrorAlert(_ r: ChatResponse) {
if !networkErrorAlert(r) {
AlertManager.shared.showAlertMsg(
private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
if let networkErrorAlert = networkErrorAlert(r) {
return networkErrorAlert
} else {
return mkAlert(
title: "Connection error",
message: "Error: \(String(describing: r))"
)
@@ -704,7 +724,9 @@ func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
title: "Connection error (AUTH)",
message: "Sender may have deleted the connection request."
)
} else if !networkErrorAlert(r) {
} else if let networkErrorAlert = networkErrorAlert(r) {
am.showAlert(networkErrorAlert)
} else {
logger.error("apiAcceptContactRequest error: \(String(describing: r))")
am.showAlertMsg(
title: "Error accepting contact request",
@@ -743,7 +765,9 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? {
title: "Cannot receive file",
message: "Sender cancelled file transfer."
)
} else if !networkErrorAlert(r) {
} else if let networkErrorAlert = networkErrorAlert(r) {
am.showAlert(networkErrorAlert)
} else {
logger.error("apiReceiveFile error: \(String(describing: r))")
switch r {
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
@@ -776,23 +800,20 @@ func apiCancelFile(fileId: Int64) async -> AChatItem? {
}
}
func networkErrorAlert(_ r: ChatResponse) -> Bool {
let am = AlertManager.shared
func networkErrorAlert(_ r: ChatResponse) -> Alert? {
switch r {
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
am.showAlertMsg(
return mkAlert(
title: "Connection timeout",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
return true
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
am.showAlertMsg(
return mkAlert(
title: "Connection error",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
return true
default:
return false
return nil
}
}
@@ -1264,6 +1285,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
case let .chatItemUpdated(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .chatItemReaction(user, r, _):
if active(user) {
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
}
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
+64 -14
View File
@@ -444,6 +444,7 @@ struct ChatView: View {
private struct ChatItemWithMenu: View {
@EnvironmentObject var chat: Chat
@Environment(\.colorScheme) var colorScheme
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
var ci: ChatItem
var showMember: Bool = false
var maxWidth: CGFloat
@@ -470,11 +471,11 @@ struct ChatView: View {
set: { _ in }
)
VStack(alignment: .trailing, spacing: 4) {
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
if ci.reactions.count > 0 {
chatItemReactions(ci.reactions)
if ci.content.msgContent != nil && ci.meta.itemDeleted == nil && ci.reactions.count > 0 {
chatItemReactions()
.padding(.bottom, 4)
}
}
@@ -505,28 +506,42 @@ struct ChatView: View {
}
}
private func chatItemReactions(_ reactions: [CIReaction]) -> some View {
private func chatItemReactions() -> some View {
HStack(spacing: 4) {
ForEach(reactions, id: \.reaction) { r in
HStack(spacing: 4) {
ForEach(ci.reactions, id: \.reaction) { r in
let v = HStack(spacing: 4) {
switch r.reaction {
case let .emoji(emoji): Text(emoji).font(.caption)
case let .emoji(emoji): Text(emoji.rawValue).font(.caption)
case .unknown: EmptyView()
}
if r.totalReacted > 1 {
Text("\(r.totalReacted)").font(.caption).foregroundColor(.secondary)
Text("\(r.totalReacted)")
.font(.caption)
.fontWeight(r.userReacted ? .bold : .light)
.foregroundColor(r.userReacted ? .accentColor : .secondary)
}
}
.padding(.horizontal, 8)
.padding(.horizontal, 6)
.padding(.vertical, 4)
.background(!r.userReacted ? Color.clear : colorScheme == .dark ? sentColorDark : sentColorLight)
.cornerRadius(16)
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
v.onTapGesture {
setReaction(r.reaction, add: !r.userReacted)
}
} else {
v
}
}
}
}
private func menu(live: Bool) -> [UIAction] {
var menu: [UIAction] = []
private func menu(live: Bool) -> [UIMenuElement] {
var menu: [UIMenuElement] = []
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction && developerTools,
let rm = reactionUIMenu() {
menu.append(rm)
}
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
menu.append(replyUIAction())
}
@@ -586,7 +601,42 @@ struct ChatView: View {
}
}
}
private func reactionUIMenu() -> UIMenu? {
let rs = MsgReaction.values.compactMap { r in
ci.reactions.contains(where: { $0.userReacted && $0.reaction == r })
? nil
: UIAction(title: r.text) { _ in setReaction(r, add: true) }
}
if rs.count > 0 {
return UIMenu(
title: NSLocalizedString("React...", comment: "chat item menu"),
image: UIImage(systemName: "hand.thumbsup"),
children: rs
)
}
return nil
}
private func setReaction(_ r: MsgReaction, add: Bool) {
Task {
do {
let chatItem = try await apiChatItemReaction(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: ci.id,
reaction: r,
add: add
)
await MainActor.run {
ChatModel.shared.updateChatItem(chat.chatInfo, chatItem)
}
} catch let error {
logger.error("apiChatItemReaction error: \(responseError(error))")
}
}
}
private func shareUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("Share", comment: "chat item action"),
@@ -12,6 +12,7 @@ import SimpleXChat
struct ContactPreferencesView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var chatModel: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var contact: Contact
@State var featuresAllowed: ContactFeaturesAllowed
@State var currentFeaturesAllowed: ContactFeaturesAllowed
@@ -24,6 +25,9 @@ struct ContactPreferencesView: View {
List {
timedMessagesFeatureSection()
featureSection(.fullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, $featuresAllowed.fullDelete)
if developerTools {
featureSection(.reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, $featuresAllowed.reactions)
}
featureSection(.voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, $featuresAllowed.voice)
featureSection(.calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, $featuresAllowed.calls)
@@ -25,14 +25,18 @@ struct GroupMemberInfoView: View {
case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
case connRequestSentAlert(type: ConnReqType)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
case other(alert: Alert)
var id: String {
switch self {
case .removeMemberAlert: return "removeMemberAlert"
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
case .connRequestSentAlert: return "connRequestSentAlert"
case let .error(title, _): return "error \(title)"
case let .other(alert): return "other \(alert)"
}
}
}
@@ -45,6 +49,15 @@ struct GroupMemberInfoView: View {
}
}
private func knownDirectChat(_ contactId: Int64) -> Chat? {
if let chat = chatModel.getContactChat(contactId),
chat.chatInfo.contact?.directOrUsed == true {
return chat
} else {
return nil
}
}
private func groupMemberInfoView() -> some View {
VStack {
List {
@@ -54,8 +67,7 @@ struct GroupMemberInfoView: View {
if member.memberActive {
Section {
if let contactId = member.memberContactId {
if let chat = chatModel.getContactChat(contactId),
chat.chatInfo.contact?.directOrUsed ?? false {
if let chat = knownDirectChat(contactId) {
knownDirectChatButton(chat)
} else if groupInfo.fullGroupPreferences.directMessages.on {
newDirectChatButton(contactId)
@@ -73,6 +85,13 @@ struct GroupMemberInfoView: View {
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
}
} header: {
Text("Address")
} footer: {
@@ -150,7 +169,28 @@ struct GroupMemberInfoView: View {
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case let .connRequestSentAlert(type): return connReqSentAlert(type)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
case let .other(alert): return alert
}
}
}
func connectViaAddressButton(_ contactLink: String) -> some View {
Button {
connectViaAddress(contactLink)
} label: {
Label("Connect", systemImage: "link")
}
}
func connectViaAddress(_ contactLink: String) {
Task {
let (connReqType, connectAlert) = await apiConnect_(connReq: contactLink)
if let connReqType = connReqType {
alert = .connRequestSentAlert(type: connReqType)
} else if let connectAlert = connectAlert {
alert = .other(alert: connectAlert)
}
}
}
@@ -12,6 +12,7 @@ import SimpleXChat
struct GroupPreferencesView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var chatModel: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var groupInfo: GroupInfo
@State var preferences: FullGroupPreferences
@State var currentPreferences: FullGroupPreferences
@@ -25,6 +26,9 @@ struct GroupPreferencesView: View {
featureSection(.timedMessages, $preferences.timedMessages.enable)
featureSection(.fullDelete, $preferences.fullDelete.enable)
featureSection(.directMessages, $preferences.directMessages.enable)
if developerTools {
featureSection(.reactions, $preferences.reactions.enable)
}
featureSection(.voice, $preferences.voice.enable)
if groupInfo.canEdit {
@@ -25,9 +25,9 @@ struct CustomTimePicker: View {
case .second: return TimeUnitLimits.init(timeUnit: .second, maxValue: 120)
case .minute: return TimeUnitLimits.init(timeUnit: .minute, maxValue: 120)
case .hour: return TimeUnitLimits.init(timeUnit: .hour, maxValue: 72)
case .day: return TimeUnitLimits.init(timeUnit: .day, maxValue: 30)
case .week: return TimeUnitLimits.init(timeUnit: .week, maxValue: 12)
case .month: return TimeUnitLimits.init(timeUnit: .month, maxValue: 3)
case .day: return TimeUnitLimits.init(timeUnit: .day, maxValue: 60)
case .week: return TimeUnitLimits.init(timeUnit: .week, maxValue: 12) // TODO in 5.2 - 54
case .month: return TimeUnitLimits.init(timeUnit: .month, maxValue: 3) // TODO in 5.2 - 12
}
}
@@ -68,7 +68,7 @@ func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
if let connReqType = await apiConnect(connReq: connectionLink) {
DispatchQueue.main.async {
dismiss?()
connectionReqSentAlert(connReqType)
AlertManager.shared.showAlert(connReqSentAlert(connReqType))
}
} else {
DispatchQueue.main.async {
@@ -111,8 +111,8 @@ func groupLinkAlert(_ connectionLink: String) -> Alert {
)
}
func connectionReqSentAlert(_ type: ConnReqType) {
AlertManager.shared.showAlertMsg(
func connReqSentAlert(_ type: ConnReqType) -> Alert {
return mkAlert(
title: "Connection request sent!",
message: type == .contact
? "You will be connected when your connection request is accepted, please wait or check later!"
@@ -11,6 +11,7 @@ import SimpleXChat
struct PreferencesView: View {
@EnvironmentObject var chatModel: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State var profile: LocalProfile
@State var preferences: FullPreferences
@State var currentPreferences: FullPreferences
@@ -20,6 +21,9 @@ struct PreferencesView: View {
List {
timedMessagesFeatureSection($preferences.timedMessages.allow)
featureSection(.fullDelete, $preferences.fullDelete.allow)
if developerTools {
featureSection(.reactions, $preferences.reactions.allow)
}
featureSection(.voice, $preferences.voice.allow)
featureSection(.calls, $preferences.calls.allow)
+20 -20
View File
@@ -63,11 +63,6 @@
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
5C80B41E2A040F23004F9B8F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C80B4192A040F23004F9B8F /* libgmpxx.a */; };
5C80B41F2A040F23004F9B8F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C80B41A2A040F23004F9B8F /* libffi.a */; };
5C80B4202A040F23004F9B8F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C80B41B2A040F23004F9B8F /* libgmp.a */; };
5C80B4212A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C80B41C2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a */; };
5C80B4222A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C80B41D2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a */; };
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
@@ -174,6 +169,11 @@
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C3B0272A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C3B0222A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a */; };
64C3B0282A1393FC00E19930 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C3B0232A1393FC00E19930 /* libffi.a */; };
64C3B0292A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C3B0242A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a */; };
64C3B02A2A1393FC00E19930 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C3B0252A1393FC00E19930 /* libgmp.a */; };
64C3B02B2A1393FC00E19930 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C3B0262A1393FC00E19930 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
@@ -313,11 +313,6 @@
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C80B4192A040F23004F9B8F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C80B41A2A040F23004F9B8F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C80B41B2A040F23004F9B8F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C80B41C2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a"; sourceTree = "<group>"; };
5C80B41D2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a"; sourceTree = "<group>"; };
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
5C84FE9229A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
@@ -446,6 +441,11 @@
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C3B0222A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a"; sourceTree = "<group>"; };
64C3B0232A1393FC00E19930 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C3B0242A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a"; sourceTree = "<group>"; };
64C3B0252A1393FC00E19930 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C3B0262A1393FC00E19930 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
@@ -493,13 +493,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C80B4212A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a in Frameworks */,
5C80B41E2A040F23004F9B8F /* libgmpxx.a in Frameworks */,
5C80B4202A040F23004F9B8F /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C80B4222A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a in Frameworks */,
5C80B41F2A040F23004F9B8F /* libffi.a in Frameworks */,
64C3B0292A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a in Frameworks */,
64C3B0272A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a in Frameworks */,
64C3B02A2A1393FC00E19930 /* libgmp.a in Frameworks */,
64C3B0282A1393FC00E19930 /* libffi.a in Frameworks */,
64C3B02B2A1393FC00E19930 /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -560,11 +560,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C80B41A2A040F23004F9B8F /* libffi.a */,
5C80B41B2A040F23004F9B8F /* libgmp.a */,
5C80B4192A040F23004F9B8F /* libgmpxx.a */,
5C80B41C2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV-ghc8.10.7.a */,
5C80B41D2A040F23004F9B8F /* libHSsimplex-chat-5.1.0.0-Zm9phGT059Fqhr8J8xPLV.a */,
64C3B0232A1393FC00E19930 /* libffi.a */,
64C3B0252A1393FC00E19930 /* libgmp.a */,
64C3B0262A1393FC00E19930 /* libgmpxx.a */,
64C3B0242A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx-ghc8.10.7.a */,
64C3B0222A1393FC00E19930 /* libHSsimplex-chat-5.1.0.0-AMORL5TbUTXHTJ22w4wnNx.a */,
);
path = Libraries;
sourceTree = "<group>";
+7 -1
View File
@@ -41,6 +41,7 @@ public enum ChatCommand {
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, reaction: MsgReaction, add: Bool)
case apiGetNtfToken
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
@@ -148,6 +149,7 @@ public enum ChatCommand {
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
case let .apiChatItemReaction(type, id, itemId, reaction, add): return "/_reaction \(ref(type, id)) \(itemId) \(reaction.cmdString) \(onOff(add))"
case .apiGetNtfToken: return "/_ntf get "
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
@@ -253,6 +255,7 @@ public enum ChatCommand {
case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem"
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
case .apiChatItemReaction: return "apiChatItemReaction"
case .apiGetNtfToken: return "apiGetNtfToken"
case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiVerifyToken"
@@ -373,7 +376,7 @@ public enum ChatCommand {
}
}
struct APIResponse: Decodable {
public struct APIResponse: Decodable {
var resp: ChatResponse
}
@@ -430,6 +433,7 @@ public enum ChatResponse: Decodable, Error {
case newChatItem(user: User, chatItem: AChatItem)
case chatItemStatusUpdated(user: User, chatItem: AChatItem)
case chatItemUpdated(user: User, chatItem: AChatItem)
case chatItemReaction(user: User, reaction: ACIReaction, added: Bool)
case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
case contactsList(user: User, contacts: [Contact])
// group events
@@ -547,6 +551,7 @@ public enum ChatResponse: Decodable, Error {
case .newChatItem: return "newChatItem"
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
case .chatItemUpdated: return "chatItemUpdated"
case .chatItemReaction: return "chatItemReaction"
case .chatItemDeleted: return "chatItemDeleted"
case .contactsList: return "contactsList"
case .groupCreated: return "groupCreated"
@@ -663,6 +668,7 @@ public enum ChatResponse: Decodable, Error {
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemReaction(u, reaction, added): return withUser(u, "added: \(added)\n\(String(describing: reaction))")
case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)")
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
+197 -37
View File
@@ -183,12 +183,20 @@ public typealias ChatId = String
public struct FullPreferences: Decodable, Equatable {
public var timedMessages: TimedMessagesPreference
public var fullDelete: SimplePreference
public var reactions: SimplePreference
public var voice: SimplePreference
public var calls: SimplePreference
public init(timedMessages: TimedMessagesPreference, fullDelete: SimplePreference, voice: SimplePreference, calls: SimplePreference) {
public init(
timedMessages: TimedMessagesPreference,
fullDelete: SimplePreference,
reactions: SimplePreference,
voice: SimplePreference,
calls: SimplePreference
) {
self.timedMessages = timedMessages
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
self.calls = calls
}
@@ -196,6 +204,7 @@ public struct FullPreferences: Decodable, Equatable {
public static let sampleData = FullPreferences(
timedMessages: TimedMessagesPreference(allow: .no),
fullDelete: SimplePreference(allow: .no),
reactions: SimplePreference(allow: .yes),
voice: SimplePreference(allow: .yes),
calls: SimplePreference(allow: .yes)
)
@@ -204,20 +213,35 @@ public struct FullPreferences: Decodable, Equatable {
public struct Preferences: Codable {
public var timedMessages: TimedMessagesPreference?
public var fullDelete: SimplePreference?
public var reactions: SimplePreference?
public var voice: SimplePreference?
public var calls: SimplePreference?
public init(timedMessages: TimedMessagesPreference?, fullDelete: SimplePreference?, voice: SimplePreference?, calls: SimplePreference?) {
public init(
timedMessages: TimedMessagesPreference?,
fullDelete: SimplePreference?,
reactions: SimplePreference?,
voice: SimplePreference?,
calls: SimplePreference?
) {
self.timedMessages = timedMessages
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
self.calls = calls
}
func copy(timedMessages: TimedMessagesPreference? = nil, fullDelete: SimplePreference? = nil, voice: SimplePreference? = nil, calls: SimplePreference? = nil) -> Preferences {
func copy(
timedMessages: TimedMessagesPreference? = nil,
fullDelete: SimplePreference? = nil,
reactions: SimplePreference? = nil,
voice: SimplePreference? = nil,
calls: SimplePreference? = nil
) -> Preferences {
Preferences(
timedMessages: timedMessages ?? self.timedMessages,
fullDelete: fullDelete ?? self.fullDelete,
reactions: reactions ?? self.reactions,
voice: voice ?? self.voice,
calls: calls ?? self.calls
)
@@ -227,6 +251,7 @@ public struct Preferences: Codable {
switch feature {
case .timedMessages: return copy(timedMessages: TimedMessagesPreference(allow: allowed, ttl: param ?? timedMessages?.ttl))
case .fullDelete: return copy(fullDelete: SimplePreference(allow: allowed))
case .reactions: return copy(reactions: SimplePreference(allow: allowed))
case .voice: return copy(voice: SimplePreference(allow: allowed))
case .calls: return copy(calls: SimplePreference(allow: allowed))
}
@@ -235,6 +260,7 @@ public struct Preferences: Codable {
public static let sampleData = Preferences(
timedMessages: TimedMessagesPreference(allow: .no),
fullDelete: SimplePreference(allow: .no),
reactions: SimplePreference(allow: .yes),
voice: SimplePreference(allow: .yes),
calls: SimplePreference(allow: .yes)
)
@@ -244,6 +270,7 @@ public func fullPreferencesToPreferences(_ fullPreferences: FullPreferences) ->
Preferences(
timedMessages: fullPreferences.timedMessages,
fullDelete: fullPreferences.fullDelete,
reactions: fullPreferences.reactions,
voice: fullPreferences.voice,
calls: fullPreferences.calls
)
@@ -253,6 +280,7 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac
Preferences(
timedMessages: contactUserPreferences.timedMessages.userPreference.preference,
fullDelete: contactUserPreferences.fullDelete.userPreference.preference,
reactions: contactUserPreferences.reactions.userPreference.preference,
voice: contactUserPreferences.voice.userPreference.preference,
calls: contactUserPreferences.calls.userPreference.preference
)
@@ -386,17 +414,20 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey {
public struct ContactUserPreferences: Decodable {
public var timedMessages: ContactUserPreference<TimedMessagesPreference>
public var fullDelete: ContactUserPreference<SimplePreference>
public var reactions: ContactUserPreference<SimplePreference>
public var voice: ContactUserPreference<SimplePreference>
public var calls: ContactUserPreference<SimplePreference>
public init(
timedMessages: ContactUserPreference<TimedMessagesPreference>,
fullDelete: ContactUserPreference<SimplePreference>,
reactions: ContactUserPreference<SimplePreference>,
voice: ContactUserPreference<SimplePreference>,
calls: ContactUserPreference<SimplePreference>
) {
self.timedMessages = timedMessages
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
self.calls = calls
}
@@ -412,6 +443,11 @@ public struct ContactUserPreferences: Decodable {
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .no)),
contactPreference: SimplePreference(allow: .no)
),
reactions: ContactUserPreference<SimplePreference>(
enabled: FeatureEnabled(forUser: true, forContact: true),
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .yes)),
contactPreference: SimplePreference(allow: .yes)
),
voice: ContactUserPreference<SimplePreference>(
enabled: FeatureEnabled(forUser: true, forContact: true),
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .yes)),
@@ -491,11 +527,10 @@ public protocol Feature {
public enum ChatFeature: String, Decodable, Feature {
case timedMessages
case fullDelete
case reactions
case voice
case calls
public var values: [ChatFeature] { [.fullDelete, .voice] }
public var id: Self { self }
public var asymmetric: Bool {
@@ -516,6 +551,7 @@ public enum ChatFeature: String, Decodable, Feature {
switch self {
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature")
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
case .calls: return NSLocalizedString("Audio/video calls", comment: "chat feature")
}
@@ -525,6 +561,7 @@ public enum ChatFeature: String, Decodable, Feature {
switch self {
case .timedMessages: return "stopwatch"
case .fullDelete: return "trash.slash"
case .reactions: return "hand.thumbsup"
case .voice: return "mic"
case .calls: return "phone"
}
@@ -534,6 +571,7 @@ public enum ChatFeature: String, Decodable, Feature {
switch self {
case .timedMessages: return "stopwatch.fill"
case .fullDelete: return "trash.slash.fill"
case .reactions: return "hand.thumbsup.fill"
case .voice: return "mic.fill"
case .calls: return "phone.fill"
}
@@ -560,6 +598,12 @@ public enum ChatFeature: String, Decodable, Feature {
case .yes: return "Allow irreversible message deletion only if your contact allows it to you."
case .no: return "Contacts can mark messages for deletion; you will be able to view them."
}
case .reactions:
switch allowed {
case .always: return "Allow your contacts adding message reactions."
case .yes: return "Allow message reactions only if your contact allows them."
case .no: return "Prohibit message reactions."
}
case .voice:
switch allowed {
case .always: return "Allow your contacts to send voice messages."
@@ -593,6 +637,14 @@ public enum ChatFeature: String, Decodable, Feature {
: enabled.forContact
? "Only your contact can irreversibly delete messages (you can mark them for deletion)."
: "Irreversible message deletion is prohibited in this chat."
case .reactions:
return enabled.forUser && enabled.forContact
? "Both you and your contact can add message reactions."
: enabled.forUser
? "Only you can add message reactions."
: enabled.forContact
? "Only your contact can add message reactions."
: "Message reactions are prohibited in this chat."
case .voice:
return enabled.forUser && enabled.forContact
? "Both you and your contact can send voice messages."
@@ -616,11 +668,10 @@ public enum ChatFeature: String, Decodable, Feature {
public enum GroupFeature: String, Decodable, Feature {
case timedMessages
case fullDelete
case reactions
case voice
case directMessages
public var values: [GroupFeature] { [.directMessages, .fullDelete, .voice] }
public var id: Self { self }
public var hasParam: Bool {
@@ -635,6 +686,7 @@ public enum GroupFeature: String, Decodable, Feature {
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
case .directMessages: return NSLocalizedString("Direct messages", comment: "chat feature")
case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature")
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
}
}
@@ -644,6 +696,7 @@ public enum GroupFeature: String, Decodable, Feature {
case .timedMessages: return "stopwatch"
case .directMessages: return "arrow.left.and.right.circle"
case .fullDelete: return "trash.slash"
case .reactions: return "hand.thumbsup"
case .voice: return "mic"
}
}
@@ -653,6 +706,7 @@ public enum GroupFeature: String, Decodable, Feature {
case .timedMessages: return "stopwatch.fill"
case .directMessages: return "arrow.left.and.right.circle.fill"
case .fullDelete: return "trash.slash.fill"
case .reactions: return "hand.thumbsup.fill"
case .voice: return "mic.fill"
}
}
@@ -682,6 +736,11 @@ public enum GroupFeature: String, Decodable, Feature {
case .on: return "Allow to irreversibly delete sent messages."
case .off: return "Prohibit irreversible message deletion."
}
case .reactions:
switch enabled {
case .on: return "Allow message reactions."
case .off: return "Prohibit messages reactions."
}
case .voice:
switch enabled {
case .on: return "Allow to send voice messages."
@@ -705,6 +764,11 @@ public enum GroupFeature: String, Decodable, Feature {
case .on: return "Group members can irreversibly delete sent messages."
case .off: return "Irreversible message deletion is prohibited in this group."
}
case .reactions:
switch enabled {
case .on: return "Group members can add message reactions."
case .off: return "Message reactions are prohibited in this group."
}
case .voice:
switch enabled {
case .on: return "Group members can send voice messages."
@@ -750,13 +814,22 @@ public struct ContactFeaturesAllowed: Equatable {
public var timedMessagesAllowed: Bool
public var timedMessagesTTL: Int?
public var fullDelete: ContactFeatureAllowed
public var reactions: ContactFeatureAllowed
public var voice: ContactFeatureAllowed
public var calls: ContactFeatureAllowed
public init(timedMessagesAllowed: Bool, timedMessagesTTL: Int?, fullDelete: ContactFeatureAllowed, voice: ContactFeatureAllowed, calls: ContactFeatureAllowed) {
public init(
timedMessagesAllowed: Bool,
timedMessagesTTL: Int?,
fullDelete: ContactFeatureAllowed,
reactions: ContactFeatureAllowed,
voice: ContactFeatureAllowed,
calls: ContactFeatureAllowed
) {
self.timedMessagesAllowed = timedMessagesAllowed
self.timedMessagesTTL = timedMessagesTTL
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
self.calls = calls
}
@@ -765,6 +838,7 @@ public struct ContactFeaturesAllowed: Equatable {
timedMessagesAllowed: false,
timedMessagesTTL: nil,
fullDelete: ContactFeatureAllowed.userDefault(.no),
reactions: ContactFeatureAllowed.userDefault(.yes),
voice: ContactFeatureAllowed.userDefault(.yes),
calls: ContactFeatureAllowed.userDefault(.yes)
)
@@ -777,6 +851,7 @@ public func contactUserPrefsToFeaturesAllowed(_ contactUserPreferences: ContactU
timedMessagesAllowed: allow == .yes || allow == .always,
timedMessagesTTL: pref.preference.ttl,
fullDelete: contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
reactions: contactUserPrefToFeatureAllowed(contactUserPreferences.reactions),
voice: contactUserPrefToFeatureAllowed(contactUserPreferences.voice),
calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls)
)
@@ -798,6 +873,7 @@ public func contactFeaturesAllowedToPrefs(_ contactFeaturesAllowed: ContactFeatu
Preferences(
timedMessages: TimedMessagesPreference(allow: contactFeaturesAllowed.timedMessagesAllowed ? .yes : .no, ttl: contactFeaturesAllowed.timedMessagesTTL),
fullDelete: contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
reactions: contactFeatureAllowedToPref(contactFeaturesAllowed.reactions),
voice: contactFeatureAllowedToPref(contactFeaturesAllowed.voice),
calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls)
)
@@ -834,12 +910,20 @@ public struct FullGroupPreferences: Decodable, Equatable {
public var timedMessages: TimedMessagesGroupPreference
public var directMessages: GroupPreference
public var fullDelete: GroupPreference
public var reactions: GroupPreference
public var voice: GroupPreference
public init(timedMessages: TimedMessagesGroupPreference, directMessages: GroupPreference, fullDelete: GroupPreference, voice: GroupPreference) {
public init(
timedMessages: TimedMessagesGroupPreference,
directMessages: GroupPreference,
fullDelete: GroupPreference,
reactions: GroupPreference,
voice: GroupPreference
) {
self.timedMessages = timedMessages
self.directMessages = directMessages
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
}
@@ -847,6 +931,7 @@ public struct FullGroupPreferences: Decodable, Equatable {
timedMessages: TimedMessagesGroupPreference(enable: .off),
directMessages: GroupPreference(enable: .off),
fullDelete: GroupPreference(enable: .off),
reactions: GroupPreference(enable: .on),
voice: GroupPreference(enable: .on)
)
}
@@ -855,12 +940,20 @@ public struct GroupPreferences: Codable {
public var timedMessages: TimedMessagesGroupPreference?
public var directMessages: GroupPreference?
public var fullDelete: GroupPreference?
public var reactions: GroupPreference?
public var voice: GroupPreference?
public init(timedMessages: TimedMessagesGroupPreference?, directMessages: GroupPreference?, fullDelete: GroupPreference?, voice: GroupPreference?) {
public init(
timedMessages: TimedMessagesGroupPreference?,
directMessages: GroupPreference?,
fullDelete: GroupPreference?,
reactions: GroupPreference?,
voice: GroupPreference?
) {
self.timedMessages = timedMessages
self.directMessages = directMessages
self.fullDelete = fullDelete
self.reactions = reactions
self.voice = voice
}
@@ -868,6 +961,7 @@ public struct GroupPreferences: Codable {
timedMessages: TimedMessagesGroupPreference(enable: .off),
directMessages: GroupPreference(enable: .off),
fullDelete: GroupPreference(enable: .off),
reactions: GroupPreference(enable: .on),
voice: GroupPreference(enable: .on)
)
}
@@ -877,6 +971,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
timedMessages: fullPreferences.timedMessages,
directMessages: fullPreferences.directMessages,
fullDelete: fullPreferences.fullDelete,
reactions: fullPreferences.reactions,
voice: fullPreferences.voice
)
}
@@ -1086,6 +1181,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
switch feature {
case .timedMessages: return cups.timedMessages.enabled.forUser
case .fullDelete: return cups.fullDelete.enabled.forUser
case .reactions: return cups.reactions.enabled.forUser
case .voice: return cups.voice.enabled.forUser
case .calls: return cups.calls.enabled.forUser
}
@@ -1094,6 +1190,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
switch feature {
case .timedMessages: return prefs.timedMessages.on
case .fullDelete: return prefs.fullDelete.on
case .reactions: return prefs.reactions.on
case .voice: return prefs.voice.on
case .calls: return false
}
@@ -1249,6 +1346,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
switch feature {
case .timedMessages: return mergedPreferences.timedMessages.contactPreference.allow != .no
case .fullDelete: return mergedPreferences.fullDelete.contactPreference.allow != .no
case .reactions: return mergedPreferences.reactions.contactPreference.allow != .no
case .voice: return mergedPreferences.voice.contactPreference.allow != .no
case .calls: return mergedPreferences.calls.contactPreference.allow != .no
}
@@ -1258,6 +1356,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
switch feature {
case .timedMessages: return mergedPreferences.timedMessages.userPreference.preference.allow != .no
case .fullDelete: return mergedPreferences.fullDelete.userPreference.preference.allow != .no
case .reactions: return mergedPreferences.reactions.userPreference.preference.allow != .no
case .voice: return mergedPreferences.voice.userPreference.preference.allow != .no
case .calls: return mergedPreferences.calls.userPreference.preference.allow != .no
}
@@ -1802,24 +1901,26 @@ public struct AChatItem: Decodable {
}
}
public struct ACIReaction: Decodable {
public var chatInfo: ChatInfo
public var chatReaction: CIReaction
}
public struct CIReaction: Decodable {
public var chatDir: CIDirection
public var chatItem: ChatItem
public var sentAt: Date
public var reaction: MsgReaction
}
public struct ChatItem: Identifiable, Decodable {
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, file: CIFile? = nil) {
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) {
self.chatDir = chatDir
self.meta = meta
self.content = content
self.formattedText = formattedText
self.quotedItem = quotedItem
self.reactions = [] // [
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: false, totalReacted: 1),
// CIReaction(reaction: .emoji(emoji: ""), userReacted: false, totalReacted: 1),
// CIReaction(reaction: .emoji(emoji: "🚀"), userReacted: false, totalReacted: 3),
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "👎"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "👀"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "🎉"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "😀"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "😕"), userReacted: true, totalReacted: 2),
// ]
self.reactions = reactions
self.file = file
}
@@ -1828,24 +1929,14 @@ public struct ChatItem: Identifiable, Decodable {
public var content: CIContent
public var formattedText: [FormattedText]?
public var quotedItem: CIQuote?
public var reactions: [CIReaction] = [] // [
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: false, totalReacted: 1),
// CIReaction(reaction: .emoji(emoji: ""), userReacted: false, totalReacted: 1),
// CIReaction(reaction: .emoji(emoji: "🚀"), userReacted: false, totalReacted: 3),
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "👎"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "👀"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "🎉"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "😀"), userReacted: true, totalReacted: 2),
// CIReaction(reaction: .emoji(emoji: "😕"), userReacted: true, totalReacted: 2),
// ]
public var reactions: [CIReactionCount]
public var file: CIFile?
public var viewTimestamp = Date.now
public var isLiveDummy: Bool = false
private enum CodingKeys: String, CodingKey {
case chatDir, meta, content, formattedText, quotedItem, file
case chatDir, meta, content, formattedText, quotedItem, reactions, file
}
public var id: Int64 { meta.itemId }
@@ -1920,6 +2011,10 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public var allowAddReaction: Bool {
meta.itemDeleted == nil && !isLiveDummy && reactions.filter({ $0.userReacted }).count < 3
}
public func autoReceiveFile() -> CIFile? {
if let file = file,
let mc = content.msgContent,
@@ -2367,14 +2462,79 @@ public struct CIQuote: Decodable, ItemContent {
}
}
public struct CIReaction: Decodable {
public struct CIReactionCount: Decodable {
public var reaction: MsgReaction
public var userReacted: Bool
public var totalReacted: Int
}
public enum MsgReaction: Decodable, Hashable {
case emoji(emoji: String)
public enum MsgReaction: Hashable {
case emoji(MREmojiChar)
case unknown
public var text: String {
switch self {
case let .emoji(emoji): return emoji.rawValue
case .unknown: return ""
}
}
public var cmdString: String {
switch self {
case let .emoji(emoji): return emoji.cmdString
case .unknown: return ""
}
}
public static var values: [MsgReaction] =
MREmojiChar.allCases.map { .emoji($0) }
enum CodingKeys: String, CodingKey {
case type
case emoji
}
}
public enum MREmojiChar: String, Codable, CaseIterable {
case thumbsup = "👍"
case thumbsdown = "👎"
case smile = "😀"
case celebration = "🎉"
case confused = "😕"
case heart = ""
case launch = "🚀"
case looking = "👀"
public var cmdString: String {
switch self {
case .thumbsup: return "+"
case .thumbsdown: return "-"
case .smile: return ")"
case .celebration: return "!"
case .confused: return "?"
case .heart: return "*"
case .launch: return "^"
case .looking: return "%"
}
}
}
extension MsgReaction: Decodable {
public init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: CodingKeys.type)
switch type {
case "emoji":
let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji)
self = .emoji(emoji)
default:
self = .unknown
}
} catch {
self = .unknown
}
}
}
public struct CIFile: Decodable {
+2
View File
@@ -84,6 +84,8 @@ deleteStorage = do
removeFile chatDb
removeFile agentDb
mapM_ removePathForcibly filesPath
tmpPath <- readTVarIO =<< asks tempDirectory
mapM_ removePathForcibly tmpPath
data StorageFiles = StorageFiles
{ chatDb :: FilePath,
+7 -6
View File
@@ -91,9 +91,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts
CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci
CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts testView
CRChatItemReaction u (ACIReaction _ _ chat reaction) added
| showReactions -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction chat reaction added ts tz
| otherwise -> []
CRChatItemReaction u (ACIReaction _ _ chat reaction) added -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction showReactions chat reaction added ts tz
CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"]
CRBroadcastSent u mc n t -> ttyUser u $ viewSentBroadcast mc n ts t
CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr
@@ -514,8 +512,8 @@ viewItemDelete chat ChatItem {chatDir, meta, content = deletedContent} toItem by
Just (AChatItem _ _ _ ci) -> chatItemDeletedText ci $ chatInfoMembership chat
prohibited = [styled (colored Red) ("[unexpected message deletion, please report to developers]" :: String)]
viewItemReaction :: forall c d. ChatInfo c -> CIReaction c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
viewItemReaction chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {chatDir = itemDir, content}, sentAt, reaction} added ts tz =
viewItemReaction :: forall c d. Bool -> ChatInfo c -> CIReaction c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {chatDir = itemDir, content}, sentAt, reaction} added ts tz =
case (chat, chatDir) of
(DirectChat c, CIDirectRcv) -> case content of
CIRcvMsgContent mc -> view from $ reactionMsg mc
@@ -534,7 +532,9 @@ viewItemReaction chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {cha
(_, CIDirectSnd) -> [sentText]
(_, CIGroupSnd) -> [sentText]
where
view from msg = viewReceivedReaction from msg reactionText ts $ utcToZonedTime tz sentAt
view from msg
| showReactions = viewReceivedReaction from msg reactionText ts $ utcToZonedTime tz sentAt
| otherwise = []
reactionText = plain $ (if added then "+ " else "- ") <> [emoji]
MREmoji (MREmojiChar emoji) = reaction
sentText = plain $ (if added then "added " else "removed ") <> [emoji]
@@ -631,6 +631,7 @@ connReqContact_ intro cReq =
"",
"Anybody can send you contact requests with: " <> highlight' "/c <contact_link_above>",
"to show it again: " <> highlight' "/sa",
"to share with your contacts: " <> highlight' "/profile_address on",
"to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)"
]
+1
View File
@@ -355,6 +355,7 @@ getContactLink cc created = do
cc <## ""
cc <## "Anybody can send you contact requests with: /c <contact_link_above>"
cc <## "to show it again: /sa"
cc <## "to share with your contacts: /profile_address on"
cc <## "to delete it: /da (accepted contacts will remain connected)"
pure link