From cd28ba62a1f5f9d2f11d2467f8b373e592e58bcf Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:13:51 +0400 Subject: [PATCH 01/89] core: fix chat pagination filtering (#911) --- src/Simplex/Chat/Store.hs | 92 ++++++++++++--------------------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index dfb2a6a7db..f04bfeca09 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -2956,7 +2956,7 @@ getGroupChatLast_ db user@User {userId} groupId count = do pure $ Chat (GroupChat groupInfo) (reverse chatItems) stats where getGroupChatItemIdsLast_ :: IO [ChatItemId] - getGroupChatItemIdsLast_ = do + getGroupChatItemIdsLast_ = map fromOnly <$> DB.query db @@ -2970,90 +2970,52 @@ getGroupChatLast_ db user@User {userId} groupId count = do (userId, groupId, count) getGroupChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user@User {userId, userContactId} groupId afterChatItemId count = do +getGroupChatAfter_ db user@User {userId} groupId afterChatItemId count = do groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId - chatItems <- ExceptT getGroupChatItemsAfter_ + afterChatItem <- getGroupChatItem db user groupId afterChatItemId + chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem) + chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds pure $ Chat (GroupChat groupInfo) chatItems stats where - getGroupChatItemsAfter_ :: IO (Either StoreError [CChatItem 'CTGroup]) - getGroupChatItemsAfter_ = do - tz <- getCurrentTimeZone - currentTs <- getCurrentTime - mapM (toGroupChatItem tz currentTs userContactId) + getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId] + getGroupChatItemIdsAfter_ afterChatItemTs = + map fromOnly <$> DB.query db [sql| - SELECT - -- ChatItem - i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, - -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, - -- GroupMember - m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, - m.member_status, m.invited_by, m.local_display_name, m.contact_id, - p.display_name, p.full_name, p.image, - -- quoted ChatItem - ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, - -- quoted GroupMember - rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, - rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, - rp.display_name, rp.full_name, rp.image - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN group_members m ON m.group_member_id = i.group_member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id - LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id - LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id - LEFT JOIN contact_profiles rp ON rp.contact_profile_id = rm.contact_profile_id - WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id > ? AND i.item_deleted != 1 - ORDER BY i.item_ts ASC, i.chat_item_id ASC + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_deleted != 1 + AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? |] - (userId, groupId, afterChatItemId, count) + (userId, groupId, afterChatItemTs, afterChatItemTs, afterChatItemId, count) getGroupChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user@User {userId, userContactId} groupId beforeChatItemId count = do +getGroupChatBefore_ db user@User {userId} groupId beforeChatItemId count = do groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId - chatItems <- ExceptT getGroupChatItemsBefore_ + beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId + chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem) + chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds pure $ Chat (GroupChat groupInfo) (reverse chatItems) stats where - getGroupChatItemsBefore_ :: IO (Either StoreError [CChatItem 'CTGroup]) - getGroupChatItemsBefore_ = do - tz <- getCurrentTimeZone - currentTs <- getCurrentTime - mapM (toGroupChatItem tz currentTs userContactId) + getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId] + getGroupChatItemIdsBefore_ beforeChatItemTs = + map fromOnly <$> DB.query db [sql| - SELECT - -- ChatItem - i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at, i.updated_at, - -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, - -- GroupMember - m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, - m.member_status, m.invited_by, m.local_display_name, m.contact_id, - p.display_name, p.full_name, p.image, - -- quoted ChatItem - ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, - -- quoted GroupMember - rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, - rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, - rp.display_name, rp.full_name, rp.image - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN group_members m ON m.group_member_id = i.group_member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id - LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id - LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id - LEFT JOIN contact_profiles rp ON rp.contact_profile_id = rm.contact_profile_id - WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id < ? AND i.item_deleted != 1 - ORDER BY i.item_ts DESC, i.chat_item_id DESC + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_deleted != 1 + AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) + ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? |] - (userId, groupId, beforeChatItemId, count) + (userId, groupId, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) getGroupChatStats_ :: DB.Connection -> UserId -> Int64 -> IO ChatStats getGroupChatStats_ db userId groupId = From 7531791f1be3af13fba9419955a4df1adfd3abff Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:48:42 +0400 Subject: [PATCH 02/89] core: chat search (#914) --- src/Simplex/Chat.hs | 13 +++--- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Store.hs | 72 ++++++++++++++++++---------------- tests/ChatTests.hs | 41 ++++++++++--------- 4 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 676ab347fe..343d248ac9 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -236,9 +236,9 @@ processChatCommand = \case APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore' $ \db -> getChatPreviews db user withPCC) - APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of - CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination) - CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\db -> getGroupChat db user cId pagination) + APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of + CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination search) + CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\db -> getGroupChat db user cId pagination search) CTContactRequest -> pure $ chatCmdError "not implemented" CTContactConnection -> pure $ chatCmdError "not supported" APIGetChatItems _pagination -> pure $ chatCmdError "not implemented" @@ -812,7 +812,7 @@ processChatCommand = \case processChatCommand . APISendMessage (ChatRef CTGroup groupId) $ ComposedMessage Nothing (Just quotedItemId) mc LastMessages (Just chatName) count -> withUser $ \user -> do chatRef <- getChatRef user chatName - CRLastMessages . aChatItems . chat <$> (processChatCommand . APIGetChat chatRef $ CPLast count) + CRLastMessages . aChatItems . chat <$> processChatCommand (APIGetChat chatRef (CPLast count) Nothing) LastMessages Nothing count -> withUser $ \user -> withStore $ \db -> CRLastMessages <$> getAllChatItems db user (CPLast count) SendFile chatName f -> withUser $ \user -> do @@ -2459,7 +2459,7 @@ chatCommandP = "/_db import " *> (APIImportArchive <$> jsonP), "/_db delete" $> APIDeleteStorage, "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), - "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP), + "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (A.space *> search)), "/_get items count=" *> (APIGetChatItems <$> A.decimal), "/_send " *> (APISendMessage <$> chatRefP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> msgContentP), @@ -2525,7 +2525,7 @@ chatCommandP = ("/contacts" <|> "/cs") $> ListContacts, ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)), ("/connect" <|> "/c") $> AddContact, - (SendMessage <$> chatNameP <* A.space <*> A.takeByteString), + SendMessage <$> chatNameP <* A.space <*> A.takeByteString, (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), ("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> A.takeByteString), @@ -2588,6 +2588,7 @@ chatCommandP = n <- (A.space *> A.takeByteString) <|> pure "" pure $ if B.null n then name else safeDecodeUtf8 n filePath = T.unpack . safeDecodeUtf8 <$> A.takeByteString + search = T.unpack . safeDecodeUtf8 <$> A.takeByteString memberRole = (" owner" $> GROwner) <|> (" admin" $> GRAdmin) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 50ec12982c..3535d99998 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -109,7 +109,7 @@ data ChatCommand | APIImportArchive ArchiveConfig | APIDeleteStorage | APIGetChats {pendingConnections :: Bool} - | APIGetChat ChatRef ChatPagination + | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems Int | APISendMessage ChatRef ComposedMessage | APIUpdateChatItem ChatRef ChatItemId MsgContent diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index f04bfeca09..49a889d673 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -2788,15 +2788,16 @@ toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Mayb toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, createdAt, updatedAt) = PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, createdAt, updatedAt} -getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChat db user contactId pagination = do +getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChat db user contactId pagination search_ = do + let search = fromMaybe "" search_ case pagination of - CPLast count -> getDirectChatLast_ db user contactId count - CPAfter afterId count -> getDirectChatAfter_ db user contactId afterId count - CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count + CPLast count -> getDirectChatLast_ db user contactId count search + CPAfter afterId count -> getDirectChatAfter_ db user contactId afterId count search + CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count search -getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatLast_ db User {userId} contactId count = do +getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatLast_ db User {userId} contactId count search = do contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsLast_ @@ -2820,14 +2821,14 @@ getDirectChatLast_ db User {userId} contactId count = do FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.item_deleted != 1 + WHERE i.user_id = ? AND i.contact_id = ? AND i.item_deleted != 1 AND i.item_text LIKE '%' || ? || '%' ORDER BY i.chat_item_id DESC LIMIT ? |] - (userId, contactId, count) + (userId, contactId, search, count) -getDirectChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatAfter_ db User {userId} contactId afterChatItemId count = do +getDirectChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatAfter_ db User {userId} contactId afterChatItemId count search = do contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsAfter_ @@ -2851,14 +2852,15 @@ getDirectChatAfter_ db User {userId} contactId afterChatItemId count = do FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.chat_item_id > ? AND i.item_deleted != 1 + WHERE i.user_id = ? AND i.contact_id = ? AND i.item_deleted != 1 AND i.item_text LIKE '%' || ? || '%' + AND i.chat_item_id > ? ORDER BY i.chat_item_id ASC LIMIT ? |] - (userId, contactId, afterChatItemId, count) + (userId, contactId, search, afterChatItemId, count) -getDirectChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatBefore_ db User {userId} contactId beforeChatItemId count = do +getDirectChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatBefore_ db User {userId} contactId beforeChatItemId count search = do contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsBefore_ @@ -2882,11 +2884,12 @@ getDirectChatBefore_ db User {userId} contactId beforeChatItemId count = do FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.chat_item_id < ? AND i.item_deleted != 1 + WHERE i.user_id = ? AND i.contact_id = ? AND i.item_deleted != 1 AND i.item_text LIKE '%' || ? || '%' + AND i.chat_item_id < ? ORDER BY i.chat_item_id DESC LIMIT ? |] - (userId, contactId, beforeChatItemId, count) + (userId, contactId, search, beforeChatItemId, count) getDirectChatStats_ :: DB.Connection -> UserId -> Int64 -> IO ChatStats getDirectChatStats_ db userId contactId = @@ -2940,15 +2943,16 @@ getContact db userId contactId = |] (userId, contactId, ConnReady, ConnSndReady) -getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChat db user groupId pagination = do +getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChat db user groupId pagination search_ = do + let search = fromMaybe "" search_ case pagination of - CPLast count -> getGroupChatLast_ db user groupId count - CPAfter afterId count -> getGroupChatAfter_ db user groupId afterId count - CPBefore beforeId count -> getGroupChatBefore_ db user groupId beforeId count + CPLast count -> getGroupChatLast_ db user groupId count search + CPAfter afterId count -> getGroupChatAfter_ db user groupId afterId count search + CPBefore beforeId count -> getGroupChatBefore_ db user groupId beforeId count search -getGroupChatLast_ :: DB.Connection -> User -> Int64 -> Int -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatLast_ db user@User {userId} groupId count = do +getGroupChatLast_ :: DB.Connection -> User -> Int64 -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatLast_ db user@User {userId} groupId count search = do groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId chatItemIds <- liftIO getGroupChatItemIdsLast_ @@ -2963,14 +2967,14 @@ getGroupChatLast_ db user@User {userId} groupId count = do [sql| SELECT chat_item_id FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_deleted != 1 + WHERE user_id = ? AND group_id = ? AND item_deleted != 1 AND item_text LIKE '%' || ? || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? |] - (userId, groupId, count) + (userId, groupId, search, count) -getGroupChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user@User {userId} groupId afterChatItemId count = do +getGroupChatAfter_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatAfter_ db user@User {userId} groupId afterChatItemId count search = do groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId afterChatItem <- getGroupChatItem db user groupId afterChatItemId @@ -2986,15 +2990,15 @@ getGroupChatAfter_ db user@User {userId} groupId afterChatItemId count = do [sql| SELECT chat_item_id FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_deleted != 1 + WHERE user_id = ? AND group_id = ? AND item_deleted != 1 AND item_text LIKE '%' || ? || '%' AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? |] - (userId, groupId, afterChatItemTs, afterChatItemTs, afterChatItemId, count) + (userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count) -getGroupChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user@User {userId} groupId beforeChatItemId count = do +getGroupChatBefore_ :: DB.Connection -> User -> Int64 -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatBefore_ db user@User {userId} groupId beforeChatItemId count search = do groupInfo <- getGroupInfo db user groupId stats <- liftIO $ getGroupChatStats_ db userId groupId beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId @@ -3010,12 +3014,12 @@ getGroupChatBefore_ db user@User {userId} groupId beforeChatItemId count = do [sql| SELECT chat_item_id FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_deleted != 1 + WHERE user_id = ? AND group_id = ? AND item_deleted != 1 AND item_text LIKE '%' || ? || '%' AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? |] - (userId, groupId, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) + (userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) getGroupChatStats_ :: DB.Connection -> UserId -> Int64 -> IO ChatStats getGroupChatStats_ db userId groupId = diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index d1497f8020..d14beb4815 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -159,11 +159,13 @@ testAddContact = versionTestMatrix2 runTestAddContact (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") chatsEmpty alice bob - alice #> "@bob hello 🙂" - bob <# "alice> hello 🙂" + alice #> "@bob hello there 🙂" + bob <# "alice> hello there 🙂" chatsOneMessage alice bob - bob #> "@alice hi" - alice <# "bob> hi" + bob #> "@alice hello there" + alice <# "bob> hello there" + bob #> "@alice how are you?" + alice <# "bob> how are you?" chatsManyMessages alice bob -- test adding the same contact one more time - local name will be different alice ##> "/c" @@ -177,15 +179,15 @@ testAddContact = versionTestMatrix2 runTestAddContact bob <# "alice_1> hello" bob #> "@alice_1 hi" alice <# "bob_1> hi" - alice @@@ [("@bob_1", "hi"), ("@bob", "hi")] - bob @@@ [("@alice_1", "hi"), ("@alice", "hi")] + alice @@@ [("@bob_1", "hi"), ("@bob", "how are you?")] + bob @@@ [("@alice_1", "hi"), ("@alice", "how are you?")] -- test deleting contact alice ##> "/d bob_1" alice <## "bob_1: contact is deleted" alice ##> "@bob_1 hey" alice <## "no contact bob_1" - alice @@@ [("@bob", "hi")] - bob @@@ [("@alice_1", "hi"), ("@alice", "hi")] + alice @@@ [("@bob", "how are you?")] + bob @@@ [("@alice_1", "hi"), ("@alice", "how are you?")] -- test clearing chat alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY") alice #$> ("/_get chat @2 count=100", chat, []) @@ -197,18 +199,20 @@ testAddContact = versionTestMatrix2 runTestAddContact bob @@@ [("@alice", "")] bob #$> ("/_get chat @2 count=100", chat, []) chatsOneMessage alice bob = do - alice @@@ [("@bob", "hello 🙂")] - alice #$> ("/_get chat @2 count=100", chat, [(1, "hello 🙂")]) - bob @@@ [("@alice", "hello 🙂")] - bob #$> ("/_get chat @2 count=100", chat, [(0, "hello 🙂")]) + alice @@@ [("@bob", "hello there 🙂")] + alice #$> ("/_get chat @2 count=100", chat, [(1, "hello there 🙂")]) + bob @@@ [("@alice", "hello there 🙂")] + bob #$> ("/_get chat @2 count=100", chat, [(0, "hello there 🙂")]) chatsManyMessages alice bob = do - alice @@@ [("@bob", "hi")] - alice #$> ("/_get chat @2 count=100", chat, [(1, "hello 🙂"), (0, "hi")]) - bob @@@ [("@alice", "hi")] - bob #$> ("/_get chat @2 count=100", chat, [(0, "hello 🙂"), (1, "hi")]) + alice @@@ [("@bob", "how are you?")] + alice #$> ("/_get chat @2 count=100", chat, [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) + bob @@@ [("@alice", "how are you?")] + bob #$> ("/_get chat @2 count=100", chat, [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) -- pagination - alice #$> ("/_get chat @2 after=1 count=100", chat, [(0, "hi")]) - alice #$> ("/_get chat @2 before=2 count=100", chat, [(1, "hello 🙂")]) + alice #$> ("/_get chat @2 after=1 count=100", chat, [(0, "hello there"), (0, "how are you?")]) + alice #$> ("/_get chat @2 before=2 count=100", chat, [(1, "hello there 🙂")]) + -- search + alice #$> ("/_get chat @2 count=100 ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages alice #$> ("/_read chat @2 from=1 to=100", id, "ok") bob #$> ("/_read chat @2 from=1 to=100", id, "ok") @@ -475,6 +479,7 @@ testGroupShared alice bob cath checkMessages = do -- so we take into account group event items as well as sent group invitations in direct chats alice #$> ("/_get chat #1 after=5 count=100", chat, [(0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 before=7 count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 count=100 team", chat, [(0, "hey team")]) bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] bob #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] From 26a019d9d24a23547241c9960bb51fbe74999197 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:43:19 +0400 Subject: [PATCH 03/89] ios: update group members in model (#915) --- .../chat/simplex/app/views/chat/ChatView.kt | 6 ++-- .../app/views/chat/group/GroupChatInfoView.kt | 4 +-- .../app/views/chatlist/ChatListNavLinkView.kt | 2 +- .../simplex/app/views/newchat/AddGroupView.kt | 4 +-- apps/ios/Shared/Model/ChatModel.swift | 29 +++++++++++++++ apps/ios/Shared/Model/SimpleXAPI.swift | 36 +++++++++++-------- apps/ios/Shared/Views/Chat/ChatView.swift | 17 +++++---- .../Chat/Group/AddGroupMembersView.swift | 23 ++++++++---- .../Views/Chat/Group/GroupChatInfoView.swift | 23 ++++++------ .../Chat/Group/GroupMemberInfoView.swift | 9 +++-- .../Shared/Views/NewChat/AddGroupView.swift | 7 +++- apps/ios/SimpleXChat/APITypes.swift | 5 +-- src/Simplex/Chat.hs | 12 +++---- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/View.hs | 2 +- 15 files changed, 122 insertions(+), 59 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index ffb3f282bd..76eb09717d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -37,7 +37,7 @@ import chat.simplex.app.views.chat.group.AddGroupMembersView import chat.simplex.app.views.chat.group.GroupChatInfoView import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.chatlist.openChat -import chat.simplex.app.views.chatlist.populateGroupMembers +import chat.simplex.app.views.chatlist.setGroupMembers import chat.simplex.app.views.helpers.* import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding @@ -106,7 +106,7 @@ fun ChatView(chatModel: ChatModel) { } } } else if (cInfo is ChatInfo.Group) { - populateGroupMembers(cInfo.groupInfo, chatModel) + setGroupMembers(cInfo.groupInfo, chatModel) ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, @@ -160,7 +160,7 @@ fun ChatView(chatModel: ChatModel) { }, addMembers = { groupInfo -> withApi { - populateGroupMembers(groupInfo, chatModel) + setGroupMembers(groupInfo, chatModel) ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index c998e99940..4ebc59d5b9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -24,7 +24,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.* -import chat.simplex.app.views.chatlist.populateGroupMembers +import chat.simplex.app.views.chatlist.setGroupMembers import chat.simplex.app.views.helpers.* @Composable @@ -43,7 +43,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { developerTools, addMembers = { withApi { - populateGroupMembers(groupInfo, chatModel) + setGroupMembers(groupInfo, chatModel) ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index c92f404075..67ef2bbe59 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -96,7 +96,7 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { } } -suspend fun populateGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { +suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId) chatModel.groupMembers.clear() chatModel.groupMembers.addAll(groupMembers) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt index 73e2c7fa0f..c4c56d65a8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt @@ -21,7 +21,7 @@ import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.ProfileNameField import chat.simplex.app.views.chat.group.AddGroupMembersView -import chat.simplex.app.views.chatlist.populateGroupMembers +import chat.simplex.app.views.chatlist.setGroupMembers import chat.simplex.app.views.helpers.* import chat.simplex.app.views.isValidDisplayName import chat.simplex.app.views.onboarding.ReadableText @@ -41,7 +41,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) chatModel.chatItems.clear() chatModel.chatId.value = groupInfo.id - populateGroupMembers(groupInfo, chatModel) + setGroupMembers(groupInfo, chatModel) close.invoke() ModalManager.shared.showCustomModal { close -> ModalView( diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 8a25d57de2..e1987c3f24 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -24,6 +24,7 @@ final class ChatModel: ObservableObject { @Published var chatId: String? @Published var chatItems: [ChatItem] = [] @Published var chatToTop: String? + @Published var groupMembers: [GroupMember] = [] // items in the terminal view @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? @@ -291,6 +292,34 @@ final class ChatModel: ObservableObject { chats.removeAll(where: { $0.id == id }) } } + + func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool { + // update current chat + if chatId == groupInfo.id { + if let i = groupMembers.firstIndex(where: { $0.id == member.id }) { + withAnimation(.default) { + self.groupMembers[i] = member + } + return false + } else { + withAnimation { groupMembers.append(member) } + return true + } + } else { + return false + } + } + + func removeGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) { + // remove from current chat + if chatId == groupInfo.id { + if let i = groupMembers.firstIndex(where: { $0.id == member.id }) { + _ = withAnimation { + self.groupMembers.remove(at: i) + } + } + } + } } final class Chat: ObservableObject, Identifiable { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 75320f91ca..0df0b5104e 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -567,20 +567,18 @@ func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo { throw r } -func addMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async { - do { - try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole) - } catch let error { - logger.error("addMember error: \(responseError(error))") - } -} - -func apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async throws { +func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws { let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) if case .sentGroupInvitation = r { return } throw r } +//func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { +// let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) +// if case let .sentGroupInvitation(_, _, member) = r { return member } +// throw r +//} + enum JoinGroupResult { case joined(groupInfo: GroupInfo) case invitationRemoved @@ -597,7 +595,7 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { } } -func apiRemoveMember(groupId: Int64, memberId: Int64) async throws -> GroupMember { +func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember { let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false) if case let .userDeletedMember(_, member) = r { return member } throw r @@ -820,12 +818,22 @@ func processReceivedMsg(_ res: ChatResponse) async { chatItems: [] )) // NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation? + case let .joinedGroupMemberConnecting(groupInfo, _, member): + _ = m.upsertGroupMember(groupInfo, member) + case let .deletedMemberUser(groupInfo, _): // TODO update user member + m.updateGroup(groupInfo) + case let .deletedMember(groupInfo, _, deletedMember): + m.removeGroupMember(groupInfo, deletedMember) + case let .leftMember(groupInfo, member): + m.removeGroupMember(groupInfo, member) + case let .groupDeleted(groupInfo, _): // TODO update user member + m.updateGroup(groupInfo) case let .userJoinedGroup(groupInfo): m.updateGroup(groupInfo) - case let .groupDeleted(groupInfo, _): - m.updateGroup(groupInfo) - case let .deletedMemberUser(groupInfo, _): - m.updateGroup(groupInfo) + case let .joinedGroupMember(groupInfo, member): + _ = m.upsertGroupMember(groupInfo, member) + case let .connectedToGroupMember(groupInfo, member): + _ = m.upsertGroupMember(groupInfo, member) case let .groupUpdated(toGroup): m.updateGroup(toGroup) case let .rcvFileStart(aChatItem): diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 0046245627..205336dbb4 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -17,7 +17,6 @@ struct ChatView: View { @ObservedObject var chat: Chat @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false - @State private var membersToAdd: [Contact] = [] @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @FocusState private var keyboardVisible: Bool @@ -110,8 +109,14 @@ struct ChatView: View { } await MainActor.run { showChatInfoSheet = true } } - } else { - showChatInfoSheet = true + } else if case let .group(groupInfo) = cInfo { + Task { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + ChatModel.shared.groupMembers = groupMembers + showChatInfoSheet = true + } + } } } label: { ChatInfoToolbar(chat: chat) @@ -138,7 +143,7 @@ struct ChatView: View { if groupInfo.canAddMembers { addMembersButton() .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: membersToAdd) + AddGroupMembersView(chat: chat, groupInfo: groupInfo) } } default: @@ -161,9 +166,9 @@ struct ChatView: View { Button { if case let .group(gInfo) = chat.chatInfo { Task { - let ms = await apiListMembers(gInfo.apiId) + let groupMembers = await apiListMembers(gInfo.groupId) await MainActor.run { - membersToAdd = filterMembersToAdd(ms) + ChatModel.shared.groupMembers = groupMembers showAddMembersSheet = true } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index bae4f60257..7e4b9cbafa 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -14,7 +14,6 @@ struct AddGroupMembersView: View { @Environment(\.dismiss) var dismiss: DismissAction var chat: Chat var groupInfo: GroupInfo - var membersToAdd: [Contact] var showSkip: Bool = false var addedMembersCb: ((Set) -> Void)? = nil @State private var selectedContacts = Set() @@ -22,6 +21,8 @@ struct AddGroupMembersView: View { var body: some View { NavigationView { + let membersToAdd = filterMembersToAdd(chatModel.groupMembers) + let v = List { ChatInfoToolbar(chat: chat, imageSize: 48) .frame(maxWidth: .infinity, alignment: .center) @@ -81,11 +82,21 @@ struct AddGroupMembersView: View { func inviteMembersButton() -> some View { Button { Task { - for contactId in selectedContacts { - await addMember(groupId: chat.chatInfo.apiId, contactId: contactId, memberRole: selectedRole) + do { + for contactId in selectedContacts { + try await apiAddMember(groupInfo.groupId, contactId, selectedRole) + // await MainActor.run { ChatModel.shared.upsertGroupMember(groupInfo, member) } + } + await MainActor.run { dismiss() } + if let cb = addedMembersCb { cb(selectedContacts) } + } catch { + AlertManager.shared.showAlert( + Alert( + title: Text("Error adding member(s)"), + message: Text(responseError(error)) + ) + ) } - await MainActor.run { dismiss() } - if let cb = addedMembersCb { cb(selectedContacts) } } } label: { HStack { @@ -132,6 +143,6 @@ struct AddGroupMembersView: View { struct AddGroupMembersView_Previews: PreviewProvider { static var previews: some View { - AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData, membersToAdd: []) + AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index e39bd323e4..7e6b2edb9a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -15,7 +15,6 @@ struct GroupChatInfoView: View { @ObservedObject var chat: Chat var groupInfo: GroupInfo @ObservedObject private var alertManager = AlertManager.shared - @State private var members: [GroupMember] = [] @State private var alert: GroupChatInfoViewAlert? = nil @State private var showAddMembersSheet: Bool = false @State private var selectedMember: GroupMember? = nil @@ -33,6 +32,10 @@ struct GroupChatInfoView: View { var body: some View { NavigationView { + let members = chatModel.groupMembers + .filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved } + .sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } + List { groupInfoHeader() .listRowBackground(Color.clear) @@ -57,7 +60,7 @@ struct GroupChatInfoView: View { } } .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: filterMembersToAdd(members)) + AddGroupMembersView(chat: chat, groupInfo: groupInfo) } .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats) @@ -96,14 +99,6 @@ struct GroupChatInfoView: View { case .leaveGroupAlert: return leaveGroupAlert() } } - .task { - let ms = await apiListMembers(chat.chatInfo.apiId) - .filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved } - .sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } - await MainActor.run { - members = ms - } - } } func groupInfoHeader() -> some View { @@ -128,7 +123,13 @@ struct GroupChatInfoView: View { private func addMembersButton() -> some View { Button { - showAddMembersSheet = true + Task { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + ChatModel.shared.groupMembers = groupMembers + showAddMembersSheet = true + } + } } label: { Label("Invite members", systemImage: "plus") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 6476a483e5..7ce57c464a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -107,10 +107,13 @@ struct GroupMemberInfoView: View { primaryButton: .destructive(Text("Remove")) { Task { do { - _ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId) - dismiss() + let member = try await apiRemoveMember(groupInfo.groupId, member.groupMemberId) + await MainActor.run { + ChatModel.shared.removeGroupMember(groupInfo, member) + dismiss() + } } catch let error { - logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)") + logger.error("apiRemoveMember error: \(responseError(error))") } } }, diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index dab36fd998..7d278114ed 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -26,7 +26,6 @@ struct AddGroupView: View { if let chat = chat, let groupInfo = groupInfo { AddGroupMembersView(chat: chat, groupInfo: groupInfo, - membersToAdd: filterMembersToAdd([]), showSkip: true) { _ in dismiss() DispatchQueue.main.async { @@ -148,6 +147,12 @@ struct AddGroupView: View { hideKeyboard() do { let gInfo = try apiNewGroup(profile) + Task { + let groupMembers = await apiListMembers(gInfo.groupId) + await MainActor.run { + ChatModel.shared.groupMembers = groupMembers + } + } let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) m.addChat(c) withAnimation { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 77c2a476b1..61e32fcef3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -256,6 +256,7 @@ public enum ChatResponse: Decodable, Error { // group events case groupCreated(groupInfo: GroupInfo) case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact) +// case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember) case userAcceptedGroupSent(groupInfo: GroupInfo) case userDeletedMember(groupInfo: GroupInfo, member: GroupMember) case leftMemberUser(groupInfo: GroupInfo) @@ -268,11 +269,11 @@ public enum ChatResponse: Decodable, Error { case leftMember(groupInfo: GroupInfo, member: GroupMember) case groupDeleted(groupInfo: GroupInfo, member: GroupMember) case contactsMerged(intoContact: Contact, mergedContact: Contact) - case groupInvitation(groupInfo: GroupInfo) + case groupInvitation(groupInfo: GroupInfo) // unused case userJoinedGroup(groupInfo: GroupInfo) case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember) case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember) - case groupRemoved(groupInfo: GroupInfo) + case groupRemoved(groupInfo: GroupInfo) // unused case groupUpdated(toGroup: GroupInfo) // receiving file events case rcvFileAccepted(chatItem: AChatItem) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 343d248ac9..2147835296 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -695,24 +695,24 @@ processChatCommand = \case when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo) unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - let sendInvitation groupMemberId memberId cReq = do + let sendInvitation member@GroupMember {groupMemberId, memberId} cReq = do let groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile msg <- sendDirectContactMessage contact $ XGrpInv groupInv let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveSndChatItem user (CDDirectSnd contact) msg content Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat contact) ci setActive $ ActiveG localDisplayName - pure $ CRSentGroupInvitation gInfo contact + pure $ CRSentGroupInvitation gInfo contact member case contactMember contact members of Nothing -> do gVar <- asks idsDrg (agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation) - GroupMember {memberId, groupMemberId} <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq - sendInvitation groupMemberId memberId cReq - Just GroupMember {groupMemberId, memberId, memberStatus} + member <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq + sendInvitation member cReq + Just member@GroupMember {groupMemberId, memberStatus} | memberStatus == GSMemInvited -> withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case - Just cReq -> sendInvitation groupMemberId memberId cReq + Just cReq -> sendInvitation member cReq Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 3535d99998..a6c0c2cc44 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -230,7 +230,7 @@ data ChatResponse | CRUserAcceptedGroupSent {groupInfo :: GroupInfo} | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} | CRGroupsList {groups :: [GroupInfo]} - | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact} + | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus | CRUserProfile {profile :: Profile} | CRUserProfileNoChange diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index e05b1dd727..df209a0394 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -90,7 +90,7 @@ responseToView testView = \case CRGroupCreated g -> viewGroupCreated g CRGroupMembers g -> viewGroupMembers g CRGroupsList gs -> viewGroupsList gs - CRSentGroupInvitation g c -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + CRSentGroupInvitation g c _ -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus CRUserProfile p -> viewUserProfile p CRUserProfileNoChange -> ["user profile did not change"] From 22733f505d5fe66dd52c6a0deac86091497ffb69 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 9 Aug 2022 19:50:29 +0400 Subject: [PATCH 04/89] android: update group members in model (#919) --- .../java/chat/simplex/app/model/ChatModel.kt | 16 ++++++++++ .../java/chat/simplex/app/model/SimpleXAPI.kt | 30 +++++++++++++++---- .../views/chat/group/AddGroupMembersView.kt | 1 + .../views/chat/group/GroupMemberInfoView.kt | 9 ++++-- apps/ios/Shared/Model/ChatModel.swift | 11 ------- apps/ios/Shared/Model/SimpleXAPI.swift | 4 +-- .../Chat/Group/GroupMemberInfoView.swift | 2 +- 7 files changed, 50 insertions(+), 23 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index fad7fc0d46..4ec82b7f1d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -248,6 +248,22 @@ class ChatModel(val controller: ChatController) { fun removeChat(id: String) { chats.removeAll { it.id == id } } + + fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean { + // update current chat + return if (chatId.value == groupInfo.id) { + val memberIndex = groupMembers.indexOfFirst { it.id == member.id } + if (memberIndex >= 0) { + groupMembers[memberIndex] = member + false + } else { + groupMembers.add(member) + true + } + } else { + false + } + } } enum class ChatType(val type: String) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index e3ee2d55f1..b3e2a681fa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -605,6 +605,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager Log.e(TAG, "apiAddMember bad response: ${r.responseType} ${r.details}") } +// suspend fun apiAddMember(groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? { +// val r = sendCmd(CC.ApiAddMember(groupId, contactId, memberRole)) +// if (r is CR.SentGroupInvitation) return r.member +// Log.e(TAG, "apiAddMember bad response: ${r.responseType} ${r.details}") +// return null +// } + suspend fun apiJoinGroup(groupId: Long) { val r = sendCmd(CC.ApiJoinGroup(groupId)) when (r) { @@ -756,12 +763,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager chatModel.addChat(Chat(chatInfo = ChatInfo.Group(r.groupInfo), chatItems = listOf())) // TODO NtfManager.shared.notifyGroupInvitation } + is CR.JoinedGroupMemberConnecting -> + chatModel.upsertGroupMember(r.groupInfo, r.member) + is CR.DeletedMemberUser -> // TODO update user member + chatModel.updateGroup(r.groupInfo) + is CR.DeletedMember -> + chatModel.upsertGroupMember(r.groupInfo, r.deletedMember) + is CR.LeftMember -> + chatModel.upsertGroupMember(r.groupInfo, r.member) + is CR.GroupDeleted -> // TODO update user member + chatModel.updateGroup(r.groupInfo) is CR.UserJoinedGroup -> chatModel.updateGroup(r.groupInfo) - is CR.GroupDeleted -> - chatModel.updateGroup(r.groupInfo) - is CR.DeletedMemberUser -> - chatModel.updateGroup(r.groupInfo) + is CR.JoinedGroupMember -> + chatModel.upsertGroupMember(r.groupInfo, r.member) + is CR.ConnectedToGroupMember -> + chatModel.upsertGroupMember(r.groupInfo, r.member) is CR.GroupUpdated -> chatModel.updateGroup(r.toGroup) is CR.RcvFileStart -> @@ -1391,6 +1408,7 @@ sealed class CR { // group events @Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR() @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact): CR() +// @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR() @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR() @@ -1403,11 +1421,11 @@ sealed class CR { @Serializable @SerialName("leftMember") class LeftMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupDeleted") class GroupDeleted(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("contactsMerged") class ContactsMerged(val intoContact: Contact, val mergedContact: Contact): CR() - @Serializable @SerialName("groupInvitation") class GroupInvitation(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupInvitation") class GroupInvitation(val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index 0411843d2d..57dc06f4c3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -42,6 +42,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> withApi { selectedContacts.forEach { chatModel.controller.apiAddMember(groupInfo.groupId, it, selectedRole.value) + // chatModel.upsertGroupMember(groupInfo, member) } close.invoke() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 1f303e63db..6a0209cc1e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -37,19 +37,22 @@ fun GroupMemberInfoView(groupInfo: GroupInfo, member: GroupMember, connStats: Co member, connStats, developerTools, - removeMember = { removeMemberDialog(member, chatModel, close) } + removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) } ) } } -fun removeMemberDialog(member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { +fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.button_remove_member), text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone), confirmText = generalGetString(R.string.remove_member_confirmation), onConfirm = { withApi { - chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId) + val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId) + if (removedMember != null) { + chatModel.upsertGroupMember(groupInfo, removedMember) + } close?.invoke() } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index e1987c3f24..b0cecea3ec 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -309,17 +309,6 @@ final class ChatModel: ObservableObject { return false } } - - func removeGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) { - // remove from current chat - if chatId == groupInfo.id { - if let i = groupMembers.firstIndex(where: { $0.id == member.id }) { - _ = withAnimation { - self.groupMembers.remove(at: i) - } - } - } - } } final class Chat: ObservableObject, Identifiable { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 0df0b5104e..fd114528a9 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -823,9 +823,9 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .deletedMemberUser(groupInfo, _): // TODO update user member m.updateGroup(groupInfo) case let .deletedMember(groupInfo, _, deletedMember): - m.removeGroupMember(groupInfo, deletedMember) + _ = m.upsertGroupMember(groupInfo, deletedMember) case let .leftMember(groupInfo, member): - m.removeGroupMember(groupInfo, member) + _ = m.upsertGroupMember(groupInfo, member) case let .groupDeleted(groupInfo, _): // TODO update user member m.updateGroup(groupInfo) case let .userJoinedGroup(groupInfo): diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 7ce57c464a..a34f5dc72d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -109,7 +109,7 @@ struct GroupMemberInfoView: View { do { let member = try await apiRemoveMember(groupInfo.groupId, member.groupMemberId) await MainActor.run { - ChatModel.shared.removeGroupMember(groupInfo, member) + _ = ChatModel.shared.upsertGroupMember(groupInfo, member) dismiss() } } catch let error { From 38f65c82c32233eaf828bd0ea4366d7fb3155165 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:46:49 +0400 Subject: [PATCH 05/89] core: send notification on XGrpMemFwd (#921) --- src/Simplex/Chat/Protocol.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 91a9f9f4bb..8b6a402288 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -452,6 +452,7 @@ hasNotification = \case XFile_ -> True XContact_ -> True XGrpInv_ -> True + XGrpMemFwd_ -> True XGrpDel_ -> True XCallInv_ -> True _ -> False From f896c4453dbbe5eee3ab5f87dc22d25da122504c Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 10 Aug 2022 14:54:15 +0400 Subject: [PATCH 06/89] mobile: update model on adding group member (#923) --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 17 +++++------------ .../app/views/chat/group/AddGroupMembersView.kt | 6 ++++-- apps/ios/Shared/Model/SimpleXAPI.swift | 10 ++-------- .../Views/Chat/Group/AddGroupMembersView.swift | 4 ++-- apps/ios/SimpleXChat/APITypes.swift | 5 ++--- 5 files changed, 15 insertions(+), 27 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b3e2a681fa..b9dc4b897f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -599,19 +599,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return null } - suspend fun apiAddMember(groupId: Long, contactId: Long, memberRole: GroupMemberRole) { + suspend fun apiAddMember(groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? { val r = sendCmd(CC.ApiAddMember(groupId, contactId, memberRole)) - if (r is CR.SentGroupInvitation) return + if (r is CR.SentGroupInvitation) return r.member Log.e(TAG, "apiAddMember bad response: ${r.responseType} ${r.details}") + return null } -// suspend fun apiAddMember(groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? { -// val r = sendCmd(CC.ApiAddMember(groupId, contactId, memberRole)) -// if (r is CR.SentGroupInvitation) return r.member -// Log.e(TAG, "apiAddMember bad response: ${r.responseType} ${r.details}") -// return null -// } - suspend fun apiJoinGroup(groupId: Long) { val r = sendCmd(CC.ApiJoinGroup(groupId)) when (r) { @@ -1407,8 +1401,7 @@ sealed class CR { @Serializable @SerialName("contactsList") class ContactsList(val contacts: List): CR() // group events @Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR() - @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact): CR() -// @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() + @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR() @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR() @@ -1577,7 +1570,7 @@ sealed class CR { is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}" is ContactsList -> json.encodeToString(contacts) is GroupCreated -> json.encodeToString(groupInfo) - is SentGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact" + is SentGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact\nmember: $member" is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is UserDeletedMember -> "groupInfo: $groupInfo\nmember: $member" is LeftMemberUser -> json.encodeToString(groupInfo) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index 57dc06f4c3..b88c0b5470 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -41,8 +41,10 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> inviteMembers = { withApi { selectedContacts.forEach { - chatModel.controller.apiAddMember(groupInfo.groupId, it, selectedRole.value) - // chatModel.upsertGroupMember(groupInfo, member) + val member = chatModel.controller.apiAddMember(groupInfo.groupId, it, selectedRole.value) + if (member != null) { + chatModel.upsertGroupMember(groupInfo, member) + } } close.invoke() } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index fd114528a9..d7dd4c7b9d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -567,18 +567,12 @@ func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo { throw r } -func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws { +func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) - if case .sentGroupInvitation = r { return } + if case let .sentGroupInvitation(_, _, member) = r { return member } throw r } -//func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { -// let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) -// if case let .sentGroupInvitation(_, _, member) = r { return member } -// throw r -//} - enum JoinGroupResult { case joined(groupInfo: GroupInfo) case invitationRemoved diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 7e4b9cbafa..d001d9ff0c 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -84,8 +84,8 @@ struct AddGroupMembersView: View { Task { do { for contactId in selectedContacts { - try await apiAddMember(groupInfo.groupId, contactId, selectedRole) - // await MainActor.run { ChatModel.shared.upsertGroupMember(groupInfo, member) } + let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole) + await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) } } await MainActor.run { dismiss() } if let cb = addedMembersCb { cb(selectedContacts) } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 61e32fcef3..1fc620c0a3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -255,8 +255,7 @@ public enum ChatResponse: Decodable, Error { case contactsList(contacts: [Contact]) // group events case groupCreated(groupInfo: GroupInfo) - case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact) -// case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember) + case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember) case userAcceptedGroupSent(groupInfo: GroupInfo) case userDeletedMember(groupInfo: GroupInfo, member: GroupMember) case leftMemberUser(groupInfo: GroupInfo) @@ -437,7 +436,7 @@ public enum ChatResponse: Decodable, Error { case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))" case let .contactsList(contacts): return String(describing: contacts) case let .groupCreated(groupInfo): return String(describing: groupInfo) - case let .sentGroupInvitation(groupInfo, contact): return "groupInfo: \(groupInfo)\ncontact: \(contact)" + case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)" case let .userAcceptedGroupSent(groupInfo): return String(describing: groupInfo) case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" case let .leftMemberUser(groupInfo): return String(describing: groupInfo) From 622ab549a378bb1354b6991b11803ac301156149 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 11 Aug 2022 02:53:02 +0300 Subject: [PATCH 07/89] Debug package suffix and ability to override Gradle variables (#920) * Debug package suffix and ability to override Gradle variables - now debug builds will have '.debug' suffix by default. It allows to have multiple app builds (debug and release) on the same device. If you don't need this, create a file local.properties and add `application_id_suffix=` into it - now everyone can override some variables from top-level build.gradle file. For example, gradle_plugin_version, debuggable manifest attribute, and so on. Overriding Gradle plugin version is useful for those who uses Intellij IDEA with older Gradle plugin than in Android Studio * Prevent socket name conflict from different packages * Configurable app name for debug build. By default it's SimpleX Debug * Changed defaults in build.gradle --- apps/android/.gitignore | 1 + apps/android/app/build.gradle | 9 +++++ apps/android/app/src/main/AndroidManifest.xml | 10 ++---- .../main/java/chat/simplex/app/SimplexApp.kt | 2 +- apps/android/build.gradle | 33 +++++++++++++++---- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/android/.gitignore b/apps/android/.gitignore index 4d1f29a2d1..e4dd4a5169 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -9,6 +9,7 @@ /.idea/assetWizardSettings.xml /.idea/deploymentTargetDropDown.xml /.idea/misc.xml +/.idea/uiDesigner.xml .DS_Store /build /captures diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 5fcf716d4a..45b0cd177c 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -26,9 +26,18 @@ android { cppFlags '' } } + manifestPlaceholders.app_name = "@string/app_name" + manifestPlaceholders.provider_authorities = "chat.simplex.app.provider" } buildTypes { + debug { + applicationIdSuffix "$application_id_suffix" + debuggable new Boolean("$enable_debuggable") + manifestPlaceholders.app_name = "$app_name" + // Provider can't be the same for different apps on the same device + manifestPlaceholders.provider_authorities = "chat.simplex.app${application_id_suffix}.provider" + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 5f2de04a24..127641dc54 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:name="SimplexApp" android:allowBackup="true" android:icon="@mipmap/icon" - android:label="@string/app_name" + android:label="${app_name}" android:supportsRtl="true" android:theme="@style/Theme.SimpleX"> @@ -34,7 +34,7 @@ android:name=".MainActivity" android:launchMode="singleTask" android:exported="true" - android:label="@string/app_name" + android:label="${app_name}" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.SimpleX"> @@ -66,9 +66,7 @@ @@ -80,9 +78,7 @@ @@ -98,7 +94,7 @@ Date: Thu, 11 Aug 2022 15:48:47 +0400 Subject: [PATCH 08/89] core: create indices on chat_items for faster pagination (#927) --- simplex-chat.cabal | 1 + .../Chat/Migrations/M20220811_chat_items_indices.hs | 13 +++++++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 11 +++++++++++ src/Simplex/Chat/Store.hs | 4 +++- 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f88141d07c..d78d21f178 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -42,6 +42,7 @@ library Simplex.Chat.Migrations.M20220626_auto_reply Simplex.Chat.Migrations.M20220702_calls Simplex.Chat.Migrations.M20220715_groups_chat_item_id + Simplex.Chat.Migrations.M20220811_chat_items_indices Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.Protocol diff --git a/src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs b/src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs new file mode 100644 index 0000000000..a43617d439 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220811_chat_items_indices where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220811_chat_items_indices :: Query +m20220811_chat_items_indices = + [sql| +CREATE INDEX idx_chat_items_groups ON chat_items(user_id, group_id, item_ts, chat_item_id); +CREATE INDEX idx_chat_items_contacts ON chat_items(user_id, contact_id, chat_item_id); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 33b5441efa..8ef4889572 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -382,3 +382,14 @@ CREATE TABLE calls( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); +CREATE INDEX idx_chat_items_groups ON chat_items( + user_id, + group_id, + item_ts, + chat_item_id +); +CREATE INDEX idx_chat_items_contacts ON chat_items( + user_id, + contact_id, + chat_item_id +); diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 49a889d673..4ab6199026 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -221,6 +221,7 @@ import Simplex.Chat.Migrations.M20220514_profiles_user_id import Simplex.Chat.Migrations.M20220626_auto_reply import Simplex.Chat.Migrations.M20220702_calls import Simplex.Chat.Migrations.M20220715_groups_chat_item_id +import Simplex.Chat.Migrations.M20220811_chat_items_indices import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -248,7 +249,8 @@ schemaMigrations = ("20220514_profiles_user_id", m20220514_profiles_user_id), ("20220626_auto_reply", m20220626_auto_reply), ("20220702_calls", m20220702_calls), - ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id) + ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), + ("20220811_chat_items_indices", m20220811_chat_items_indices) ] -- | The list of migrations in ascending order by date From db367b376bc6d418f003ebb52a0c07347dcd6140 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:30:51 +0300 Subject: [PATCH 09/89] build-android: add bundle script (#931) --- scripts/android/build-android-bundle.sh | 27 +++++++++++++++++++++++++ scripts/android/lib.txt | 2 ++ 2 files changed, 29 insertions(+) create mode 100755 scripts/android/build-android-bundle.sh create mode 100644 scripts/android/lib.txt diff --git a/scripts/android/build-android-bundle.sh b/scripts/android/build-android-bundle.sh new file mode 100755 index 0000000000..18aa03145a --- /dev/null +++ b/scripts/android/build-android-bundle.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +# Safety measures +[ -n "$1" ] || exit 1 +set -eu + +tmp=$(mktemp -d -t) +libsim=$(cat "$1" | grep libsimplex) +libsup=$(cat "$1" | grep libsupport) +commit="${2:-nix-android}" + +# Clone simplex +git clone https://github.com/simplex-chat/simplex-chat "$tmp/simplex-chat" + +# Switch to nix-android branch +git -C "$tmp/simplex-chat" checkout "$commit" + +# Create missing folders +mkdir -p "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a" + +curl -sSf "$libsim" -o "$tmp/libsimplex.zip" +unzip -o "$tmp/libsimplex.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a" + +curl -sSf "$libsup" -o "$tmp/libsupport.zip" +unzip -o "$tmp/libsupport.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a" + +gradle -p "$tmp/simplex-chat/apps/android/" clean build +cp "$tmp/simplex-chat/apps/android/app/build/outputs/apk/release/app-release-unsigned.apk" "$PWD/simplex-chat.apk" diff --git a/scripts/android/lib.txt b/scripts/android/lib.txt new file mode 100644 index 0000000000..ae196262c9 --- /dev/null +++ b/scripts/android/lib.txt @@ -0,0 +1,2 @@ +https://ci.zw3rk.com/build/494539/download/1/pkg-aarch64-android-libsimplex.zip +https://ci.zw3rk.com/build/482524/download/1/pkg-aarch64-android-libsupport.zip From 57000fa3f0db2d09938601bbe465576d5f9bf818 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 12 Aug 2022 22:16:33 +0300 Subject: [PATCH 10/89] Endless scrolling in a chat view (#925) * Endless scrolling in a chat view - scroll position when you open keyboard/change screen orientation will remain the same - scrolling to top will show messages from history - unread messages will be positioned at the top of the screen * Marking chat read message-per-message * Prevent changing scroll position on orientation change * Adapted new code to the old code * refactor Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 20 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 8 +- .../chat/simplex/app/views/chat/ChatView.kt | 172 +++++++++++++----- .../app/views/chatlist/ChatListNavLinkView.kt | 13 +- 4 files changed, 158 insertions(+), 55 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 4ec82b7f1d..761196a966 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -211,27 +211,39 @@ class ChatModel(val controller: ChatController) { } } - fun markChatItemsRead(cInfo: ChatInfo) { + fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null) { + val markedRead = markItemsReadInCurrentChat(cInfo, range) // update preview val chatIdx = getChatIndex(cInfo.id) if (chatIdx >= 0) { val chat = chats[chatIdx] val lastId = chat.chatItems.lastOrNull()?.id if (lastId != null) { - chats[chatIdx] = chat.copy(chatStats = chat.chatStats.copy(unreadCount = 0, minUnreadItemId = lastId + 1)) + chats[chatIdx] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = if (range != null) chat.chatStats.unreadCount - markedRead else 0, + // Can't use minUnreadItemId currently since chat items can have unread items between read items + //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 + ) + ) } } - // update current chat + } + + private fun markItemsReadInCurrentChat(cInfo: ChatInfo, range: CC.ItemRange? = null): Int { + var markedRead = 0 if (chatId.value == cInfo.id) { var i = 0 while (i < chatItems.count()) { val item = chatItems[i] - if (item.meta.itemStatus is CIStatus.RcvNew) { + if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { chatItems[i] = item.withStatus(CIStatus.RcvRead()) + markedRead++ } i += 1 } } + return markedRead } // func popChat(_ id: String) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b9dc4b897f..f13e824e65 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -314,7 +314,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("failed getting the list of chats: ${r.responseType} ${r.details}") } - suspend fun apiGetChat(type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(100)): Chat? { + suspend fun apiGetChat(type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT)): Chat? { val r = sendCmd(CC.ApiGetChat(type, id, pagination)) if (r is CR.ApiChat ) return r.chat Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") @@ -1276,6 +1276,12 @@ sealed class ChatPagination { is After -> "after=${this.chatItemId} count=${this.count}" is Before -> "before=${this.chatItemId} count=${this.count}" } + + companion object { + const val INITIAL_COUNT = 100 + const val PRELOAD_COUNT = 20 + const val UNTIL_PRELOAD_COUNT = 10 + } } @Serializable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 76eb09717d..b9f83ffa5e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.chat import android.content.res.Configuration -import android.util.Log import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.* @@ -20,8 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -29,24 +27,23 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.app.R -import chat.simplex.app.TAG import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* import chat.simplex.app.views.chat.group.AddGroupMembersView import chat.simplex.app.views.chat.group.GroupChatInfoView import chat.simplex.app.views.chat.item.ChatItemView -import chat.simplex.app.views.chatlist.openChat -import chat.simplex.app.views.chatlist.setGroupMembers +import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.helpers.* import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel) { - val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } + var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) } @@ -54,26 +51,12 @@ fun ChatView(chatModel: ChatModel) { val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() - if (chat == null || user == null) { + if (activeChat == null || user == null) { chatModel.chatId.value = null } else { + val chat = activeChat!! BackHandler { chatModel.chatId.value = null } - // TODO a more advanced version would mark as read only if in view - LaunchedEffect(chat.chatItems) { - Log.d(TAG, "ChatView ${chatModel.chatId.value}: LaunchedEffect") - delay(750L) - if (chat.chatItems.isNotEmpty()) { - chatModel.markChatItemsRead(chat.chatInfo) - chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) - withApi { - chatModel.controller.apiChatRead( - chat.chatInfo.chatType, - chat.chatInfo.apiId, - CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id) - ) - } - } - } + ChatLayout( user, chat, @@ -122,7 +105,20 @@ fun ChatView(chatModel: ChatModel) { val c = chatModel.chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId } - if (c != null) withApi { openChat(c.chatInfo, chatModel) } + if (c != null) { + withApi { + openChat(c.chatInfo, chatModel) + // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly + activeChat = c + } + } + }, + loadPrevMessages = { cInfo -> + val c = chatModel.getChat(cInfo.id) + val firstId = chatModel.chatItems.firstOrNull()?.id + if (c != null && firstId != null) { + withApi { apiLoadPrevMessages(firstId, c.chatInfo, chatModel) } + } }, deleteMessage = { itemId, mode -> withApi { @@ -170,6 +166,17 @@ fun ChatView(chatModel: ChatModel) { } } } + }, + markRead = { range -> + chatModel.markChatItemsRead(chat.chatInfo, range) + chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + withApi { + chatModel.controller.apiChatRead( + chat.chatInfo.chatType, + chat.chatInfo.apiId, + range + ) + } } ) } @@ -189,12 +196,14 @@ fun ChatLayout( back: () -> Unit, info: () -> Unit, openDirectChat: (Long) -> Unit, + loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, joinGroup: (Long) -> Unit, startCall: (CallMediaType) -> Unit, acceptCall: (Contact) -> Unit, - addMembers: (GroupInfo) -> Unit + addMembers: (GroupInfo) -> Unit, + markRead: (CC.ItemRange) -> Unit, ) { Surface( Modifier @@ -219,8 +228,10 @@ fun ChatLayout( bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> - Box(Modifier.padding(contentPadding)) { - ChatItemsList(user, chat, composeState, chatItems, useLinkPreviews, openDirectChat, deleteMessage, receiveFile, joinGroup, acceptCall) + BoxWithConstraints(Modifier.padding(contentPadding)) { + ChatItemsList(user, chat, composeState, chatItems, + useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, + receiveFile, joinGroup, acceptCall, markRead) } } } @@ -323,34 +334,57 @@ val CIListStateSaver = run { } @Composable -fun ChatItemsList( +fun BoxWithConstraintsScope.ChatItemsList( user: User, chat: Chat, composeState: MutableState, chatItems: List, useLinkPreviews: Boolean, openDirectChat: (Long) -> Unit, + loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, joinGroup: (Long) -> Unit, - acceptCall: (Contact) -> Unit + acceptCall: (Contact) -> Unit, + markRead: (CC.ItemRange) -> Unit, ) { - val listState = rememberLazyListState(initialFirstVisibleItemIndex = chatItems.size - chatItems.count { it.isRcvNew }) - val keyboardState by getKeyboardState() - val ciListState = rememberSaveable(stateSaver = CIListStateSaver) { - mutableStateOf(CIListState(false, chatItems.count(), keyboardState)) - } + val firstVisibleOffset = -with(LocalDensity.current) { maxHeight.roundToPx() } + + // Places first unread message at the top of a screen + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0), + initialFirstVisibleItemScrollOffset = firstVisibleOffset + ) val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current val cxt = LocalContext.current - LazyColumn(state = listState) { - itemsIndexed(chatItems) { i, cItem -> - if (i == 0) { - Spacer(Modifier.size(8.dp)) + + // Prevent scrolling to bottom on orientation change + var shouldAutoScroll by rememberSaveable { mutableStateOf(true) } + LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType) { + val firstUnreadIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0) + if (shouldAutoScroll && listState.firstVisibleItemIndex != firstUnreadIndex) { + scope.launch { + // Places first unread message at the top of a screen after moving from group to direct chat + listState.scrollToItem(firstUnreadIndex, firstVisibleOffset) } + } + // Don't autoscroll next time until it will be needed + shouldAutoScroll = false + } + + PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, chat, chatItems) { c -> + loadPrevMessages(c.chatInfo) + } + + Spacer(Modifier.size(8.dp)) + + val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } } + LazyColumn(state = listState, reverseLayout = true) { + itemsIndexed(reversedChatItems) { i, cItem -> if (chat.chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { - val prevItem = if (i > 0) chatItems[i - 1] else null + val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null val member = cItem.chatDir.groupMember val showMember = showMemberImage(member, prevItem) Row(Modifier.padding(start = 8.dp, end = 66.dp)) { @@ -362,7 +396,11 @@ fun ChatItemsList( Box( Modifier .clip(CircleShape) - .clickable { openDirectChat(contactId) } + .clickable { + openDirectChat(contactId) + // Scroll to first unread message when direct chat will be loaded + shouldAutoScroll = true + } ) { MemberImage(member) } @@ -389,17 +427,49 @@ fun ChatItemsList( ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) } } - } - val len = chatItems.count() - if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) { - scope.launch { - ciListState.value = CIListState(true, len, keyboardState) - listState.animateScrollToItem(len - 1) + + if (cItem.isRcvNew) { + LaunchedEffect(cItem.id) { + scope.launch { + delay(750) + markRead(CC.ItemRange(cItem.id, cItem.id)) + } + } } } } } +@Composable +fun PreloadItems( + listState: LazyListState, + remaining: Int = 10, + chat: Chat, + items: List<*>, + onLoadMore: (chat: Chat) -> Unit, +) { + val loadMore = remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + if (lastVisibleItemIndex > (totalItemsNumber - remaining)) + totalItemsNumber + else + 0 + } + } + + LaunchedEffect(loadMore, chat, items) { + snapshotFlow { loadMore.value } + .distinctUntilChanged() + .filter { it > 0 } + .collect { + onLoadMore(chat) + } + } +} + fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean { return prevItem == null || prevItem.chatDir is CIDirection.GroupSnd || (prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId) @@ -454,12 +524,14 @@ fun PreviewChatLayout() { back = {}, info = {}, openDirectChat = {}, + loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, receiveFile = {}, joinGroup = {}, startCall = {}, acceptCall = { _ -> }, - addMembers = { _ -> } + addMembers = { _ -> }, + markRead = { _ -> }, ) } } @@ -503,12 +575,14 @@ fun PreviewGroupChatLayout() { back = {}, info = {}, openDirectChat = {}, + loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, receiveFile = {}, joinGroup = {}, startCall = {}, acceptCall = { _ -> }, - addMembers = { _ -> } + addMembers = { _ -> }, + markRead = { _ -> }, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 67ef2bbe59..6e3ec9f3a5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -96,6 +96,13 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { } } +suspend fun apiLoadPrevMessages(beforeChatItemId: Long, chatInfo: ChatInfo, chatModel: ChatModel) { + val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) + val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination) ?: return + chatModel.chatItems.addAll(0, chat.chatItems) + chatModel.chatId.value = chatInfo.id +} + suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId) chatModel.groupMembers.clear() @@ -247,12 +254,16 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: } fun markChatRead(chat: Chat, chatModel: ChatModel) { + // Just to be sure + if (chat.chatStats.unreadCount == 0) return + + val minUnreadItemId = chat.chatStats.minUnreadItemId chatModel.markChatItemsRead(chat.chatInfo) withApi { chatModel.controller.apiChatRead( chat.chatInfo.chatType, chat.chatInfo.apiId, - CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id) + CC.ItemRange(minUnreadItemId, chat.chatItems.last().id) ) } } From e43be1ad8b43a929fd6ecdb4a6157a8808b06e1d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 13 Aug 2022 11:53:53 +0100 Subject: [PATCH 11/89] core: support multiple hostnames in server addresses (#930) * core: support multiple hostnames in server addresses * add onion hosts * update simplexmq, fix test * fix parsing servers with multiple hostnames --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 12 ++++++------ src/Simplex/Chat/Options.hs | 4 ++-- src/Simplex/Chat/Store.hs | 6 ++++-- src/Simplex/Chat/Terminal.hs | 8 ++++---- src/Simplex/Chat/View.hs | 11 ++++++----- stack.yaml | 2 +- tests/ChatClient.hs | 2 +- tests/ChatTests.hs | 2 +- tests/ProtocolTests.hs | 23 ++++++++++++----------- 11 files changed, 39 insertions(+), 35 deletions(-) diff --git a/cabal.project b/cabal.project index 1b7635644a..e5758b4551 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 7d99c4b35cf2dc531219bc83146b714c9bae429c + tag: 68138c08d29ba27cbf23a29cf9c2ab6653173685 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 64d384c741..d03c78f2d0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."7d99c4b35cf2dc531219bc83146b714c9bae429c" = "037a0p7cdi4lrsbh21b4gldwdcj1sk8wz4wjsph6bnv2jyid11gk"; + "https://github.com/simplex-chat/simplexmq.git"."68138c08d29ba27cbf23a29cf9c2ab6653173685" = "0b9yhbd3jsrbfnf1asvi2wwyqy65fq31h1f76s6sx5rcyir33ibb"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2147835296..50579578e3 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -105,13 +105,13 @@ defaultChatConfig = _defaultSMPServers :: NonEmpty SMPServer _defaultSMPServers = L.fromList - [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im", - "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im", - "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im" + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion" ] _defaultNtfServers :: [NtfServer] -_defaultNtfServers = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"] +_defaultNtfServers = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"] maxImageSize :: Integer maxImageSize = 236700 @@ -1252,10 +1252,10 @@ processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of SUSPENDED -> toView CRChatSuspended _ -> pure () where - serverEvent srv@(SMPServer host port _) conns event str = do + serverEvent srv@(SMPServer host _ _) conns event str = do cs <- withStore' $ \db -> getConnectionsContacts db userId conns toView $ event srv cs - showToast ("server " <> str) (safeDecodeUtf8 . strEncode $ SrvLoc host port) + showToast ("server " <> str) (safeDecodeUtf8 $ strEncode host) processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage = (withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus) >>= \case RcvDirectMsgConnection conn contact_ -> diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index cbd35e7dd9..04c23692b5 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -52,7 +52,7 @@ chatOpts appDir defaultDbFileName = do ( long "server" <> short 's' <> metavar "SERVER" - <> help "Comma separated list of SMP server(s) to use" + <> help "Semicolon-separated list of SMP server(s) to use (each server can have more than one hostname)" <> value [] ) socksProxy <- @@ -151,7 +151,7 @@ serverPortP :: A.Parser (Maybe String) serverPortP = Just . B.unpack <$> A.takeWhile A.isDigit smpServersP :: A.Parser [SMPServer] -smpServersP = strP `A.sepBy1` A.char ',' +smpServersP = strP `A.sepBy1` A.char ';' getChatOpts :: FilePath -> FilePath -> IO ChatOpts getChatOpts appDir defaultDbFileName = diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 4ab6199026..de0abc8d21 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -193,6 +193,7 @@ import Data.Function (on) import Data.Functor (($>)) import Data.Int (Int64) import Data.List (find, sortBy, sortOn) +import Data.List.NonEmpty (NonEmpty) import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Ord (Down (..)) import Data.Text (Text) @@ -231,6 +232,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) +import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM @@ -2799,7 +2801,7 @@ getDirectChat db user contactId pagination search_ = do CPBefore beforeId count -> getDirectChatBefore_ db user contactId beforeId count search getDirectChatLast_ :: DB.Connection -> User -> Int64 -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatLast_ db User {userId} contactId count search = do +getDirectChatLast_ db User {userId} contactId count search = do contact <- getContact db userId contactId stats <- liftIO $ getDirectChatStats_ db userId contactId chatItems <- ExceptT getDirectChatItemsLast_ @@ -3692,7 +3694,7 @@ getSMPServers db User {userId} = |] (Only userId) where - toSmpServer :: (String, String, C.KeyHash) -> SMPServer + toSmpServer :: (NonEmpty TransportHost, String, C.KeyHash) -> SMPServer toSmpServer (host, port, keyHash) = SMPServer host port keyHash overwriteSMPServers :: DB.Connection -> User -> [SMPServer] -> ExceptT StoreError IO () diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index f1554f576b..66a35fc138 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -26,11 +26,11 @@ terminalChatConfig = InitialAgentServers { smp = L.fromList - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im" + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ], - ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im"], + ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], netCfg = defaultNetworkConfig } } diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index df209a0394..c8f92d78ca 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -129,8 +129,8 @@ responseToView testView = \case CRContactConnecting _ -> [] CRContactConnected ct -> [ttyFullContact ct <> ": contact is connected"] CRContactAnotherClient c -> [ttyContact' c <> ": contact is connected to another client"] - CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> smpServer srv <> " (" <> contactList cs <> ")"] - CRContactsSubscribed srv cs -> [plain $ "server connected " <> smpServer srv <> " (" <> contactList cs <> ")"] + CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] CRContactSubError c e -> [ttyContact' c <> ": contact error " <> sShow e] CRContactSubSummary summary -> [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" @@ -201,11 +201,12 @@ responseToView testView = \case _ -> Nothing viewErrorsSummary :: [a] -> StyledString -> [StyledString] viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] - smpServer :: SMPServer -> String - smpServer SMP.ProtocolServer {host, port} = B.unpack . strEncode $ SrvLoc host port contactList :: [ContactRef] -> String contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs +showSMPServer :: SMPServer -> String +showSMPServer = B.unpack . strEncode . host + viewChatItem :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> [StyledString] viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} doShow = case chat of DirectChat c -> case chatDir of @@ -526,7 +527,7 @@ viewServers :: [SMPServer] -> StyledString viewServers = plain . intercalate ", " . map (B.unpack . strEncode) viewServerHosts :: [SMPServer] -> StyledString -viewServerHosts = plain . intercalate ", " . map host +viewServerHosts = plain . intercalate ", " . map showSMPServer viewUserProfileUpdated :: Profile -> Profile -> [StyledString] viewUserProfileUpdated Profile {displayName = n, fullName, image} Profile {displayName = n', fullName = fullName', image = image'} diff --git a/stack.yaml b/stack.yaml index 56b03e35fc..f441840ee5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 7d99c4b35cf2dc531219bc83146b714c9bae429c + commit: 68138c08d29ba27cbf23a29cf9c2ab6653173685 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 6410130836..0f15c0e7b2 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -91,7 +91,7 @@ testCfg = testAgentCfgV1 :: AgentConfig testAgentCfgV1 = testAgentCfg - { smpAgentVersion = 1, + { smpClientVRange = mkVersionRange 1 1, smpAgentVRange = mkVersionRange 1 1, smpCfg = (smpCfg testAgentCfg) {smpServerVRange = mkVersionRange 1 1} } diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index d14beb4815..046ee25cf0 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2052,7 +2052,7 @@ testGetSetSMPServers = alice #$> ("/smp_servers", id, "no custom SMP servers saved") alice #$> ("/smp_servers smp://1234-w==@smp1.example.im", id, "ok") alice #$> ("/smp_servers", id, "smp://1234-w==@smp1.example.im") - alice #$> ("/smp_servers smp://2345-w==@smp2.example.im,smp://3456-w==@smp3.example.im:5224", id, "ok") + alice #$> ("/smp_servers smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok") alice #$> ("/smp_servers", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224") alice #$> ("/smp_servers default", id, "ok") alice #$> ("/smp_servers", id, "no custom SMP servers saved") diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index e7ffcc2d07..d829b74228 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -15,7 +15,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (smpClientVRange) +import Simplex.Messaging.Protocol (supportedSMPClientVRange) import Simplex.Messaging.Version import Test.Hspec @@ -28,11 +28,12 @@ srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") queue :: SMPQueueUri queue = SMPQueueUri - { smpServer = srv, - senderId = "\223\142z\251", - clientVRange = smpClientVRange, - dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" - } + supportedSMPClientVRange + SMPQueueAddress + { smpServer = srv, + senderId = "\223\142z\251", + dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" + } connReqData :: ConnReqUriData connReqData = @@ -154,7 +155,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Just testConnReq} it "x.file without file invitation" $ "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" @@ -163,7 +164,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") testConnReq "photo.jpg" it "x.file.cancel" $ "{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" @@ -187,7 +188,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\"},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\"},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile} it "x.grp.acpt" $ "{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" @@ -199,10 +200,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} it "x.grp.mem.inv" $ - "{\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq} it "x.grp.mem.fwd" $ - "{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}" + "{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq} it "x.grp.mem.info" $ "{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" From aac80dacf79167811544494582b9fc37a9aa9757 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 13 Aug 2022 14:18:12 +0100 Subject: [PATCH 12/89] core: host connection events --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 8 ++++++-- src/Simplex/Chat/Controller.hs | 6 +++++- src/Simplex/Chat/Mobile.hs | 1 + src/Simplex/Chat/Options.hs | 13 ++++++++++--- src/Simplex/Chat/View.hs | 11 ++++++++++- stack.yaml | 2 +- tests/ChatClient.hs | 1 + 9 files changed, 36 insertions(+), 10 deletions(-) diff --git a/cabal.project b/cabal.project index e5758b4551..102caeff3a 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 68138c08d29ba27cbf23a29cf9c2ab6653173685 + tag: 02bba01c1661e59b5bf6e443d759ca601ffa1cd8 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index d03c78f2d0..b6b0f19e92 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."68138c08d29ba27cbf23a29cf9c2ab6653173685" = "0b9yhbd3jsrbfnf1asvi2wwyqy65fq31h1f76s6sx5rcyir33ibb"; + "https://github.com/simplex-chat/simplexmq.git"."02bba01c1661e59b5bf6e443d759ca601ffa1cd8" = "1ziryv40jg0w32yv122xn8s9c6xijzahw1dzwyzi1s1sv9016nhm"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 50579578e3..477b71a16d 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -99,6 +99,7 @@ defaultChatConfig = fileChunkSize = 15780, subscriptionConcurrency = 16, subscriptionEvents = False, + hostEvents = False, testView = False } @@ -123,9 +124,9 @@ logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} newChatController :: SQLiteStore -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController -newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, smpServers, networkConfig, logConnections} sendToast = do +newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, smpServers, networkConfig, logConnections, logServerHosts} sendToast = do let f = chatStoreFile dbFilePrefix - config = cfg {subscriptionEvents = logConnections} + config = cfg {subscriptionEvents = logConnections, hostEvents = logServerHosts} sendNotification = fromMaybe (const $ pure ()) sendToast activeTo <- newTVarIO ActiveNone firstTime <- not <$> doesFileExist f @@ -1247,11 +1248,14 @@ subscribeUserConnections agentBatchSubscribe user = do processAgentMessage :: forall m. ChatMonad m => Maybe User -> ConnId -> ACommand 'Agent -> m () processAgentMessage Nothing _ _ = throwChatError CENoActiveUser processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of + CONNECT p h -> hostEvent $ CRHostConnected p h + DISCONNECT p h -> hostEvent $ CRHostDisconnected p h DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected" UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected" SUSPENDED -> toView CRChatSuspended _ -> pure () where + hostEvent = whenM (asks $ hostEvents . config) . toView serverEvent srv@(SMPServer host _ _) conns event str = do cs <- withStore' $ \db -> getConnectionsContacts db userId conns toView $ event srv cs diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index a6c0c2cc44..a606cf2f11 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -40,8 +40,9 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Protocol (CorrId, MsgFlags) +import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags) import Simplex.Messaging.TMap (TMap) +import Simplex.Messaging.Transport.Client (TransportHost) import System.IO (Handle) import UnliftIO.STM @@ -62,6 +63,7 @@ data ChatConfig = ChatConfig fileChunkSize :: Integer, subscriptionConcurrency :: Int, subscriptionEvents :: Bool, + hostEvents :: Bool, testView :: Bool } @@ -269,6 +271,8 @@ data ChatResponse | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} | CRContactSubError {contact :: Contact, chatError :: ChatError} | CRContactSubSummary {contactSubscriptions :: [ContactSubStatus]} + | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} + | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRGroupInvitation {groupInfo :: GroupInfo} | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember} diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 714734c747..5e862f315c 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -70,6 +70,7 @@ mobileChatOpts = smpServers = [], networkConfig = defaultNetworkConfig, logConnections = False, + logServerHosts = True, logAgent = False, chatCmd = "", chatCmdDelay = 3, diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 04c23692b5..24f809a4a1 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -28,6 +28,7 @@ data ChatOpts = ChatOpts smpServers :: [SMPServer], networkConfig :: NetworkConfig, logConnections :: Bool, + logServerHosts :: Bool, logAgent :: Bool, chatCmd :: String, chatCmdDelay :: Int, @@ -56,12 +57,12 @@ chatOpts appDir defaultDbFileName = do <> value [] ) socksProxy <- - flag' (Just defaultSocksProxy) (short 'x' <> help "use local SOCKS5 proxy at :9050") + flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050") <|> option parseSocksProxy ( long "socks-proxy" <> metavar "SOCKS5" - <> help "`ipv4:port` or `:port` of SOCKS5 proxy" + <> help "Use SOCKS5 proxy at `ipv4:port` or `:port`" <> value Nothing ) t <- @@ -78,10 +79,15 @@ chatOpts appDir defaultDbFileName = do <> short 'c' <> help "Log every contact and group connection on start" ) + logServerHosts <- + switch + ( long "log-hosts" + <> short 'l' + <> help "Log connections to servers" + ) logAgent <- switch ( long "log-agent" - <> short 'l' <> help "Enable logs from SMP agent" ) chatCmd <- @@ -123,6 +129,7 @@ chatOpts appDir defaultDbFileName = do smpServers, networkConfig = fullNetworkConfig socksProxy $ useTcpTimeout socksProxy t, logConnections, + logServerHosts, logAgent, chatCmd, chatCmdDelay, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index c8f92d78ca..426f22b66e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -14,6 +14,7 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Char (toUpper) import Data.Function (on) import Data.Int (Int64) import Data.List (groupBy, intercalate, intersperse, partition, sortOn) @@ -42,8 +43,9 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (ProtocolServer (..)) +import Simplex.Messaging.Protocol (AProtocolType, ProtocolServer (..)) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (bshow) import System.Console.ANSI.Types @@ -136,6 +138,8 @@ responseToView testView = \case [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" where (errors, subscribed) = partition (isJust . contactError) summary + CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] + CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} -> [groupInvitation' ldn fullName] CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role @@ -207,6 +211,9 @@ responseToView testView = \case showSMPServer :: SMPServer -> String showSMPServer = B.unpack . strEncode . host +viewHostEvent :: AProtocolType -> TransportHost -> String +viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h) + viewChatItem :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> [StyledString] viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} doShow = case chat of DirectChat c -> case chatDir of @@ -863,6 +870,8 @@ viewChatError = \case \ secured with different credentials, or due to a bug - please re-create the connection" ] AGENT A_DUPLICATE -> [] + AGENT A_PROHIBITED -> [] + CONN NOT_FOUND -> [] e -> ["smp agent error: " <> sShow e] where fileNotFound fileId = ["file " <> sShow fileId <> " not found"] diff --git a/stack.yaml b/stack.yaml index f441840ee5..13b86dd771 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 68138c08d29ba27cbf23a29cf9c2ab6653173685 + commit: 02bba01c1661e59b5bf6e443d759ca601ffa1cd8 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 0f15c0e7b2..6edb5e99f8 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -51,6 +51,7 @@ testOpts = smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"], networkConfig = defaultNetworkConfig, logConnections = False, + logServerHosts = False, logAgent = False, chatCmd = "", chatCmdDelay = 3, From 5d8d636adcd9f5fca711d8164d42c88bdd36c632 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 14 Aug 2022 00:00:26 +0300 Subject: [PATCH 13/89] Floating button with unread counter and go to bottom action (#929) * Floating buttons with unread counters and go to bottom action * Fixed marking read of long chats without preloaded messages * Apply suggestions from code review * Counters fix * update button size/color Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 4 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 4 +- .../chat/simplex/app/views/chat/ChatView.kt | 210 +++++++++++++++--- .../app/views/chatlist/ChatPreviewView.kt | 7 +- .../app/views/helpers/GestureDetector.kt | 65 +++++- 5 files changed, 251 insertions(+), 39 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 761196a966..291299bc15 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -211,7 +211,7 @@ class ChatModel(val controller: ChatController) { } } - fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null) { + fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { val markedRead = markItemsReadInCurrentChat(cInfo, range) // update preview val chatIdx = getChatIndex(cInfo.id) @@ -221,7 +221,7 @@ class ChatModel(val controller: ChatController) { if (lastId != null) { chats[chatIdx] = chat.copy( chatStats = chat.chatStats.copy( - unreadCount = if (range != null) chat.chatStats.unreadCount - markedRead else 0, + unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0, // Can't use minUnreadItemId currently since chat items can have unread items between read items //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index f13e824e65..e2c469e47a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1279,8 +1279,8 @@ sealed class ChatPagination { companion object { const val INITIAL_COUNT = 100 - const val PRELOAD_COUNT = 20 - const val UNTIL_PRELOAD_COUNT = 10 + const val PRELOAD_COUNT = 100 + const val UNTIL_PRELOAD_COUNT = 50 } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index b9f83ffa5e..572bc6944a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -4,12 +4,14 @@ import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver @@ -24,8 +26,7 @@ import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* @@ -57,9 +58,18 @@ fun ChatView(chatModel: ChatModel) { val chat = activeChat!! BackHandler { chatModel.chatId.value = null } + // We need to have real unreadCount value for displaying it inside top right button + // Having activeChat reloaded on every change in it is inefficient (UI lags) + val unreadCount = remember { + derivedStateOf { + chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 + } + } + ChatLayout( user, chat, + unreadCount, composeState, composeView = { if (chat.chatInfo.sendMsgEnabled) { @@ -167,8 +177,8 @@ fun ChatView(chatModel: ChatModel) { } } }, - markRead = { range -> - chatModel.markChatItemsRead(chat.chatInfo, range) + markRead = { range, unreadCountAfter -> + chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter) chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) withApi { chatModel.controller.apiChatRead( @@ -186,6 +196,7 @@ fun ChatView(chatModel: ChatModel) { fun ChatLayout( user: User, chat: Chat, + unreadCount: State, composeState: MutableState, composeView: (@Composable () -> Unit), attachmentOption: MutableState, @@ -203,7 +214,7 @@ fun ChatLayout( startCall: (CallMediaType) -> Unit, acceptCall: (Contact) -> Unit, addMembers: (GroupInfo) -> Unit, - markRead: (CC.ItemRange) -> Unit, + markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, ) { Surface( Modifier @@ -223,15 +234,19 @@ fun ChatLayout( sheetState = attachmentBottomSheetState, sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { + val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } Scaffold( topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers) }, bottomBar = composeView, - modifier = Modifier.navigationBarsWithImePadding() + modifier = Modifier.navigationBarsWithImePadding(), + floatingActionButton = floatingButton.value, ) { contentPadding -> BoxWithConstraints(Modifier.padding(contentPadding)) { - ChatItemsList(user, chat, composeState, chatItems, + ChatItemsList( + user, chat, unreadCount, composeState, chatItems, useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, - receiveFile, joinGroup, acceptCall, markRead) + receiveFile, joinGroup, acceptCall, markRead, floatingButton + ) } } } @@ -337,6 +352,7 @@ val CIListStateSaver = run { fun BoxWithConstraintsScope.ChatItemsList( user: User, chat: Chat, + unreadCount: State, composeState: MutableState, chatItems: List, useLinkPreviews: Boolean, @@ -346,28 +362,20 @@ fun BoxWithConstraintsScope.ChatItemsList( receiveFile: (Long) -> Unit, joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, - markRead: (CC.ItemRange) -> Unit, + markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + floatingButton: MutableState<@Composable () -> Unit> ) { - val firstVisibleOffset = -with(LocalDensity.current) { maxHeight.roundToPx() } - - // Places first unread message at the top of a screen - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0), - initialFirstVisibleItemScrollOffset = firstVisibleOffset - ) + val listState = rememberLazyListState() val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current val cxt = LocalContext.current - // Prevent scrolling to bottom on orientation change + // Helps to scroll to bottom after moving from Group to Direct chat + // and prevents scrolling to bottom on orientation change var shouldAutoScroll by rememberSaveable { mutableStateOf(true) } LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType) { - val firstUnreadIndex = kotlin.math.max(kotlin.math.min(chatItems.size - 1, chatItems.count { it.isRcvNew }), 0) - if (shouldAutoScroll && listState.firstVisibleItemIndex != firstUnreadIndex) { - scope.launch { - // Places first unread message at the top of a screen after moving from group to direct chat - listState.scrollToItem(firstUnreadIndex, firstVisibleOffset) - } + if (shouldAutoScroll && listState.firstVisibleItemIndex != 0) { + scope.launch { listState.scrollToItem(0) } } // Don't autoscroll next time until it will be needed shouldAutoScroll = false @@ -432,12 +440,89 @@ fun BoxWithConstraintsScope.ChatItemsList( LaunchedEffect(cItem.id) { scope.launch { delay(750) - markRead(CC.ItemRange(cItem.id, cItem.id)) + markRead(CC.ItemRange(cItem.id, cItem.id), null) } } } } } + FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, markRead, floatingButton, listState) +} + +@Composable +fun BoxWithConstraintsScope.FloatingButtons( + chatItems: List, + unreadCount: State, + minUnreadItemId: Long, + markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + floatingButton: MutableState<@Composable () -> Unit>, + listState: LazyListState +) { + val scope = rememberCoroutineScope() + + val bottomUnreadCount by remember { derivedStateOf { + chatItems.subList( + chatItems.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex, + chatItems.size + ).count { it.isRcvNew } } + } + + val firstItemIsVisible by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } + val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() + + LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { + val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible + val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible + floatingButton.value = bottomEndFloatingButton( + bottomUnreadCount, + showButtonWithCounter, + showButtonWithArrow, + onClickArrowDown = { + scope.launch { listState.animateScrollToItem(0) } + }, + onClickCounter = { + scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } + } + ) + } + + val fabSize = 56.dp + val topUnreadCount by remember { + derivedStateOf { unreadCount.value - bottomUnreadCount } + } + val showButtonWithCounter = topUnreadCount > 0 + val height = with(LocalDensity.current) { maxHeight.toPx() } + var showDropDown by remember { mutableStateOf(false) } + + TopEndFloatingButton( + Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd), + topUnreadCount, + showButtonWithCounter, + onClick = { scope.launch { listState.animateScrollBy(height) } }, + onLongClick = { showDropDown = true } + ) + + DropdownMenu( + expanded = showDropDown, + onDismissRequest = { showDropDown = false }, + Modifier.width(220.dp), + offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize) + ) { + DropdownMenuItem( + onClick = { + markRead( + CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + bottomUnreadCount + ) + showDropDown = false + } + ) { + Text( + generalGetString(R.string.mark_read), + maxLines = 1, + ) + } + } } @Composable @@ -480,6 +565,75 @@ fun MemberImage(member: GroupMember) { ProfileImage(38.dp, member.memberProfile.image) } +@Composable +private fun TopEndFloatingButton( + modifier: Modifier = Modifier, + unreadCount: Int, + showButtonWithCounter: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit +) = when { + showButtonWithCounter -> { + val interactionSource = interactionSourceWithDetection(onClick, onLongClick) + FloatingActionButton( + {}, // no action here + modifier.size(48.dp), + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), + interactionSource = interactionSource, + ) { + Text( + unreadCountStr(unreadCount), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) + } + } + else -> { + } +} + +private fun bottomEndFloatingButton( + unreadCount: Int, + showButtonWithCounter: Boolean, + showButtonWithArrow: Boolean, + onClickArrowDown: () -> Unit, + onClickCounter: () -> Unit +): @Composable () -> Unit = when { + showButtonWithCounter -> { + { + FloatingActionButton( + onClick = onClickCounter, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.size(48.dp) + ) { + Text( + unreadCountStr(unreadCount), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) + } + } + } + showButtonWithArrow -> { + { + FloatingActionButton( + onClick = onClickArrowDown, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.size(48.dp) + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } + } + } + else -> { + {} + } +} + @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -507,6 +661,7 @@ fun PreviewChatLayout() { 6, CIDirection.DirectRcv(), Clock.System.now(), "hello" ) ) + val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } ChatLayout( user = User.sampleData, chat = Chat( @@ -514,6 +669,7 @@ fun PreviewChatLayout() { chatItems = chatItems, chatStats = Chat.ChatStats() ), + unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, attachmentOption = remember { mutableStateOf(null) }, @@ -531,7 +687,7 @@ fun PreviewChatLayout() { startCall = {}, acceptCall = { _ -> }, addMembers = { _ -> }, - markRead = { _ -> }, + markRead = { _, _ -> }, ) } } @@ -558,6 +714,7 @@ fun PreviewGroupChatLayout() { 6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello" ) ) + val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } ChatLayout( user = User.sampleData, chat = Chat( @@ -565,6 +722,7 @@ fun PreviewGroupChatLayout() { chatItems = chatItems, chatStats = Chat.ChatStats() ), + unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, attachmentOption = remember { mutableStateOf(null) }, @@ -582,7 +740,7 @@ fun PreviewGroupChatLayout() { startCall = {}, acceptCall = { _ -> }, addMembers = { _ -> }, - markRead = { _ -> }, + markRead = { _, _ -> }, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index ec5a2d94b2..65161deeec 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -140,7 +140,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { contentAlignment = Alignment.Center ) { Text( - if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation), + unreadCountStr(n), color = MaterialTheme.colors.onPrimary, fontSize = 11.sp, modifier = Modifier @@ -163,6 +163,11 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } } +@Composable +fun unreadCountStr(n: Int): String { + return if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation) +} + @Composable fun ChatStatusImage(chat: Chat) { val s = chat.serverInfo.networkStatus diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt index 306d7113b0..e29f2de9c0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt @@ -16,7 +16,13 @@ package chat.simplex.app.views.helpers +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.interaction.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException @@ -31,19 +37,19 @@ import androidx.compose.ui.input.pointer.consumeAllChanges import androidx.compose.ui.input.pointer.consumeDownChange import androidx.compose.ui.input.pointer.isOutOfBounds import androidx.compose.ui.input.pointer.positionChangeConsumed +import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.Density import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import chat.simplex.app.TAG +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex /** * See original code here: [androidx.compose.foundation.gestures.detectTapGestures] * */ - -interface PressGestureScope : Density { +interface PressGestureScope: Density { suspend fun tryAwaitRelease(): Boolean } @@ -67,7 +73,6 @@ suspend fun PointerInputScope.detectGesture( if (onPress !== NoPressGesture) launch { pressScope.onPress(down.position) } - val longPressTimeout = onLongPress?.let { viewConfiguration.longPressTimeoutMillis } ?: (Long.MAX_VALUE / 2) @@ -81,7 +86,6 @@ suspend fun PointerInputScope.detectGesture( } else { if (shouldConsume) upOrCancel.consumeDownChange() - // If onLongPress event is needed, cancel short press event if (onLongPress != null) pressScope.cancel() @@ -138,7 +142,6 @@ suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange ) { return null } - val consumeCheck = awaitPointerEvent(PointerEventPass.Final) if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) { return null @@ -148,7 +151,7 @@ suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange private class PressGestureScopeImpl( density: Density -) : PressGestureScope, Density by density { +): PressGestureScope, Density by density { private var isReleased = false private var isCanceled = false private val mutex = Mutex(locked = false) @@ -176,3 +179,49 @@ private class PressGestureScopeImpl( return isCanceled } } + +/** + * Captures click events and calls [onLongClick] or [onClick] when such even happens. Otherwise, does nothing. + * Apply [MutableInteractionSource] to any element that allows to pass it in (for example, in [Modifier.clickable]). + * Works in situations when using [Modifier.combinedClickable] doesn't work because external element overrides [Modifier.clickable] + * */ +@Composable +fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit): MutableInteractionSource { + val interactionSource = remember { MutableInteractionSource() } + val longPressTimeoutMillis = LocalViewConfiguration.current.longPressTimeoutMillis + var topLevelInteraction: Interaction? by remember { mutableStateOf(null) } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + topLevelInteraction = interaction + } + } + LaunchedEffect(topLevelInteraction is PressInteraction.Press) { + if (topLevelInteraction !is PressInteraction.Press) return@LaunchedEffect + try { + withTimeout(longPressTimeoutMillis) { + while (isActive) { + delay(10) + when (topLevelInteraction) { + is PressInteraction.Press -> {} + is PressInteraction.Release -> { + onClick(); break + } + is PressInteraction.Cancel -> break + } + } + } + } catch (_: TimeoutCancellationException) { + // Long click happened + onLongClick() + } catch (ex: CancellationException) { + // Canceled coroutine + PressInteraction.Release == short click + if (topLevelInteraction is PressInteraction.Release) + onClick() + Log.e(TAG, ex.stackTraceToString()) + } catch (ex: Exception) { + // Should never be called + Log.e(TAG, ex.stackTraceToString()) + } + } + return interactionSource +} From 7f78b081003e043e732a18ee486c600ec684aff6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 14 Aug 2022 17:34:11 +0100 Subject: [PATCH 14/89] mobile: add host mode in NetCfg (#933) --- .../src/main/java/chat/simplex/app/model/SimpleXAPI.kt | 9 +++++++++ apps/ios/SimpleXChat/APITypes.swift | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index e2c469e47a..7cf7053630 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1293,6 +1293,8 @@ class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = @Serializable data class NetCfg( val socksProxy: String? = null, + val hostMode: HostMode = HostMode.OnionViaSocks, + val requiredHostMode: Boolean = false, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpKeepAlive: KeepAliveOpts?, @@ -1322,6 +1324,13 @@ data class NetCfg( } } +@Serializable +enum class HostMode { + @SerialName("onionViaSocks") OnionViaSocks, + @SerialName("onion") Onion, + @SerialName("public") Public; +} + @Serializable data class KeepAliveOpts( val keepIdle: Int, // seconds diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1fc620c0a3..746e0366d8 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -516,6 +516,8 @@ public struct ArchiveConfig: Encodable { public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil + public var hostMode: HostMode = .publicHost + public var requiredHostMode = false public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpKeepAlive: KeepAliveOpts? @@ -540,6 +542,12 @@ public struct NetCfg: Codable, Equatable { public var enableKeepAlive: Bool { tcpKeepAlive != nil } } +public enum HostMode: String, Codable { + case onionViaSocks + case onionHost = "onion" + case publicHost = "public" +} + public struct KeepAliveOpts: Codable, Equatable { public var keepIdle: Int // seconds public var keepIntvl: Int // seconds From 954338658f2e7b7c7601c26e3a10b5e4c36b3125 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 14 Aug 2022 21:13:57 +0100 Subject: [PATCH 15/89] core: update simplexmq (fix ntf server hosts) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 102caeff3a..9f07946f90 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 02bba01c1661e59b5bf6e443d759ca601ffa1cd8 + tag: 04377962328b0b9e722d502f5cab91a189f43dbc source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b6b0f19e92..f697c34804 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."02bba01c1661e59b5bf6e443d759ca601ffa1cd8" = "1ziryv40jg0w32yv122xn8s9c6xijzahw1dzwyzi1s1sv9016nhm"; + "https://github.com/simplex-chat/simplexmq.git"."04377962328b0b9e722d502f5cab91a189f43dbc" = "0y3mgg7li5xg3dk4cfh46gvaqrg34rxskv9qp2jbjxh7p6iaj670"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 13b86dd771..cd2be0e807 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 02bba01c1661e59b5bf6e443d759ca601ffa1cd8 + commit: 04377962328b0b9e722d502f5cab91a189f43dbc # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 2e4ffb7fe987d98cc5bd1a381fec04499c64ab10 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 15 Aug 2022 08:25:41 +0100 Subject: [PATCH 16/89] ios: setting to use .onion hosts (#934) --- .../UserSettings/NetworkAndServers.swift | 85 +++++++++++++++++++ apps/ios/SimpleXChat/APITypes.swift | 36 +++++++- apps/ios/SimpleXChat/AppGroup.swift | 13 +++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index 8969dd95f4..6b91c02391 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -7,9 +7,27 @@ // import SwiftUI +import SimpleXChat + +enum OnionHostsAlert: Identifiable { + case update(hosts: OnionHosts) + case error(err: String) + + var id: String { + switch self { + case let .update(hosts): return "update \(hosts)" + case let .error(err): return "error \(err)" + } + } +} struct NetworkAndServers: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @State private var cfgLoaded = false + @State private var currentNetCfg = NetCfg.defaults + @State private var netCfg = NetCfg.defaults + @State private var onionHosts: OnionHosts = .no + @State private var showOnionHostsAlert: OnionHostsAlert? var body: some View { VStack { @@ -22,6 +40,10 @@ struct NetworkAndServers: View { settingsRow("server.rack") { Text("SMP servers") } } + Picker("Use .onion hosts", selection: $onionHosts) { + ForEach(OnionHosts.values, id: \.self) { Text($0.text) } + } + if developerTools { NavigationLink { AdvancedNetworkSettings() @@ -33,6 +55,69 @@ struct NetworkAndServers: View { } } } + .onAppear { + if cfgLoaded { return } + cfgLoaded = true + currentNetCfg = getNetCfg() + resetNetCfgView() + } + .onChange(of: onionHosts) { _ in + if onionHosts != OnionHosts(netCfg: currentNetCfg) { + showOnionHostsAlert = .update(hosts: onionHosts) + } + } + .alert(item: $showOnionHostsAlert) { a in + switch a { + case let .update(hosts): + return Alert( + title: Text("Update .onion hosts setting?"), + message: Text(onionHostsInfo()) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), + primaryButton: .default(Text("Ok")) { + saveNetCfg(hosts) + }, + secondaryButton: .cancel() { + resetNetCfgView() + } + ) + case let .error(err): + return Alert( + title: Text("Error updating settings"), + message: Text(err) + ) + } + } + } + + private func saveNetCfg(_ hosts: OnionHosts) { + do { + let (hostMode, requiredHostMode) = hosts.hostMode + netCfg.hostMode = hostMode + netCfg.requiredHostMode = requiredHostMode + let def = netCfg.hostMode == .onionHost ? NetCfg.proxyDefaults : NetCfg.defaults + netCfg.tcpConnectTimeout = def.tcpConnectTimeout + netCfg.tcpTimeout = def.tcpTimeout + try setNetworkConfig(netCfg) + currentNetCfg = netCfg + setNetCfg(netCfg) + } catch let error { + let err = responseError(error) + resetNetCfgView() + showOnionHostsAlert = .error(err: err) + logger.error("\(err)") + } + } + + private func resetNetCfgView() { + netCfg = currentNetCfg + onionHosts = OnionHosts(netCfg: netCfg) + } + + private func onionHostsInfo() -> LocalizedStringKey { + switch onionHosts { + case .no: return "Onion hosts will not be used." + case .prefer: return "Onion hosts will be used when available. Requires enabling VPN." + case .require: return "Onion hosts will be required for connection. Requires enabling VPN." + } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 746e0366d8..f34a12524e 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -517,7 +517,7 @@ public struct ArchiveConfig: Encodable { public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil public var hostMode: HostMode = .publicHost - public var requiredHostMode = false + public var requiredHostMode = true public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpKeepAlive: KeepAliveOpts? @@ -548,6 +548,40 @@ public enum HostMode: String, Codable { case publicHost = "public" } +public enum OnionHosts: String, Identifiable { + case no + case prefer + case require + + public var text: LocalizedStringKey { + switch self { + case .no: return "No" + case .prefer: return "When available" + case .require: return "Requred" + } + } + + public var hostMode: (HostMode, Bool) { + switch self { + case .no: return (.publicHost, true) + case .prefer: return (.onionHost, false) + case .require: return (.onionHost, true) + } + } + + public init(netCfg: NetCfg) { + switch netCfg.hostMode { + case .onionViaSocks: self = .no + case .onionHost: self = netCfg.requiredHostMode ? .require : .prefer + case .publicHost: self = .no + } + } + + public var id: OnionHosts { self } + + public static let values: [OnionHosts] = [.no, .prefer, .require] +} + public struct KeepAliveOpts: Codable, Equatable { public var keepIdle: Int // seconds public var keepIntvl: Int // seconds diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index b63db62154..15ff7ee52a 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -15,6 +15,7 @@ public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" +let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" @@ -29,6 +30,7 @@ public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)! public func registerGroupDefaults() { groupDefaults.register(defaults: [ + GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, @@ -84,6 +86,12 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) +public let networkUseOnionHostsGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS, + withDefault: .no +) + public class DateDefault { var defaults: UserDefaults var key: String @@ -157,6 +165,8 @@ public class Default { } public func getNetCfg() -> NetCfg { + let onionHosts = networkUseOnionHostsGroupDefault.get() + let (hostMode, requiredHostMode) = onionHosts.hostMode let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) @@ -171,6 +181,8 @@ public func getNetCfg() -> NetCfg { tcpKeepAlive = nil } return NetCfg( + hostMode: hostMode, + requiredHostMode: requiredHostMode, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpKeepAlive: tcpKeepAlive, @@ -179,6 +191,7 @@ public func getNetCfg() -> NetCfg { } public func setNetCfg(_ cfg: NetCfg) { + networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg)) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) From 3776e1c29c536fa445796dc91a9fc18c244706db Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 15 Aug 2022 21:07:11 +0100 Subject: [PATCH 17/89] ios: chat pagination (#910) * ios: chat pagination * pagination hack * rotationEffect * more rotation * the least broken context menu * custom contect menu * add context item menus * fix context menu preview size * fix content menu targeted previews * subclass context menu view * remove UIView subclass * move coordinator class inside view * context menu and clicks work * reverse model * update item view based on viewId * hide underlying swiftui item * cover swiftui item with solid color * remove overlay * move hostview to async block * background overlay * remove async hostview * clear chat items on back buttom * update viewId on status changes --- apps/ios/Shared/Model/ChatModel.swift | 35 +-- apps/ios/Shared/Model/SimpleXAPI.swift | 17 +- apps/ios/Shared/Views/Chat/ChatView.swift | 270 ++++++++++++------ .../Shared/Views/Helpers/ContextMenu.swift | 88 ++++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 21 ++ .../xcshareddata/swiftpm/Package.resolved | 9 + apps/ios/SimpleXChat/ChatTypes.swift | 27 +- 7 files changed, 348 insertions(+), 119 deletions(-) create mode 100644 apps/ios/Shared/Views/Helpers/ContextMenu.swift diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index b0cecea3ec..175e9c387e 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -22,7 +22,7 @@ final class ChatModel: ObservableObject { @Published var chats: [Chat] = [] // current chat @Published var chatId: String? - @Published var chatItems: [ChatItem] = [] + @Published var reversedChatItems: [ChatItem] = [] @Published var chatToTop: String? @Published var groupMembers: [GroupMember] = [] // items in the terminal view @@ -159,7 +159,7 @@ final class ChatModel: ObservableObject { } // add to current chat if chatId == cInfo.id { - withAnimation { chatItems.append(cItem) } + withAnimation { reversedChatItems.insert(cItem, at: 0) } if case .rcvNew = cItem.meta.itemStatus { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if self.chatId == cInfo.id { @@ -187,13 +187,14 @@ final class ChatModel: ObservableObject { } // update current chat if chatId == cInfo.id { - if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) { + if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) { withAnimation(.default) { - self.chatItems[i] = cItem + self.reversedChatItems[i] = cItem + self.reversedChatItems[i].viewTimestamp = .now } return false } else { - withAnimation { chatItems.append(cItem) } + withAnimation { reversedChatItems.insert(cItem, at: 0) } return true } } else { @@ -210,12 +211,12 @@ final class ChatModel: ObservableObject { } // remove from current chat if chatId == cInfo.id { - if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) { - if chatItems[i].isRcvNew() == true { + if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) { + if reversedChatItems[i].isRcvNew() == true { NtfManager.shared.decNtfBadgeCount() } _ = withAnimation { - self.chatItems.remove(at: i) + self.reversedChatItems.remove(at: i) } } } @@ -230,9 +231,10 @@ final class ChatModel: ObservableObject { // update current chat if chatId == cInfo.id { var i = 0 - while i < chatItems.count { - if case .rcvNew = chatItems[i].meta.itemStatus { - chatItems[i].meta.itemStatus = .rcvRead + while i < reversedChatItems.count { + if case .rcvNew = reversedChatItems[i].meta.itemStatus { + reversedChatItems[i].meta.itemStatus = .rcvRead + reversedChatItems[i].viewTimestamp = .now } i = i + 1 } @@ -249,7 +251,7 @@ final class ChatModel: ObservableObject { } // clear current chat if chatId == cInfo.id { - chatItems = [] + reversedChatItems = [] } } @@ -259,8 +261,9 @@ final class ChatModel: ObservableObject { chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 } // update current chat - if chatId == cInfo.id, let j = chatItems.firstIndex(where: { $0.id == cItem.id }) { - chatItems[j].meta.itemStatus = .rcvRead + if chatId == cInfo.id, let j = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) { + reversedChatItems[j].meta.itemStatus = .rcvRead + reversedChatItems[j].viewTimestamp = .now } } @@ -269,8 +272,8 @@ final class ChatModel: ObservableObject { } func getPrevChatItem(_ ci: ChatItem) -> ChatItem? { - if let i = chatItems.firstIndex(where: { $0.id == ci.id }), i > 0 { - return chatItems[i - 1] + if let i = reversedChatItems.firstIndex(where: { $0.id == ci.id }), i < reversedChatItems.count - 1 { + return reversedChatItems[i + 1] } else { return nil } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d7dd4c7b9d..0bc3f9a5ee 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -185,19 +185,25 @@ func apiGetChats() throws -> [ChatData] { throw r } -func apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination = .last(count: 100)) throws -> Chat { - let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: pagination)) +func apiGetChat(type: ChatType, id: Int64) throws -> Chat { + let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50))) if case let .apiChat(chat) = r { return Chat.init(chat) } throw r } +func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination) async throws -> [ChatItem] { + let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination)) + if case let .apiChat(chat) = r { return chat.chatItems } + throw r +} + func loadChat(chat: Chat) { do { let cInfo = chat.chatInfo let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId) let m = ChatModel.shared m.updateChatInfo(chat.chatInfo) - m.chatItems = chat.chatItems + m.reversedChatItems = chat.chatItems.reversed() } catch let error { logger.error("loadChat error: \(responseError(error))") } @@ -548,10 +554,11 @@ func markChatRead(_ chat: Chat) async { func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { do { + logger.debug("apiMarkChatItemRead: \(cItem.id)") try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - DispatchQueue.main.async { ChatModel.shared.markChatItemRead(cInfo, cItem) } + await MainActor.run { ChatModel.shared.markChatItemRead(cInfo, cItem) } } catch { - logger.error("markChatItemRead apiChatRead error: \(responseError(error))") + logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))") } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 205336dbb4..5522be3c44 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -8,6 +8,7 @@ import SwiftUI import SimpleXChat +import Introspect private let memberImageSize: CGFloat = 34 @@ -22,6 +23,10 @@ struct ChatView: View { @FocusState private var keyboardVisible: Bool @State private var showDeleteMessage = false @State private var connectionStats: ConnectionStats? + @State private var tableView: UITableView? + @State private var loadingItems = false + @State private var firstPage = false + @State private var scrolledToUnread = false var body: some View { let cInfo = chat.chatInfo @@ -35,47 +40,34 @@ struct ChatView: View { ScrollViewReader { proxy in ScrollView { LazyVStack(spacing: 5) { - ForEach(chatModel.chatItems) { ci in - if case let .groupRcv(member) = ci.chatDir { - let prevItem = chatModel.getPrevChatItem(ci) - HStack(alignment: .top, spacing: 0) { - let showMember = prevItem == nil || showMemberImage(member, prevItem) - if showMember { - ProfileImage(imageStr: member.memberProfile.image) - .frame(width: memberImageSize, height: memberImageSize) - } else { - Rectangle().fill(.clear) - .frame(width: memberImageSize, height: memberImageSize) - } - chatItemWithMenu(ci, maxWidth, showMember: showMember).padding(.leading, 8) - } - .padding(.trailing) - .padding(.leading, 12) - } else { - chatItemWithMenu(ci, maxWidth).padding(.horizontal) - } + ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in + chatItemView(ci, maxWidth) + .scaleEffect(x: 1, y: -1, anchor: .center) + .onAppear { loadChatItems(cInfo, ci, proxy) } } - .onAppear { - DispatchQueue.main.async { - scrollToFirstUnread(proxy) - } - markAllRead() - } - .onChange(of: chatModel.chatItems.last?.id) { _ in - scrollToBottom(proxy) - } - .onChange(of: keyboardVisible) { _ in - if keyboardVisible { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - scrollToBottom(proxy, animation: .easeInOut(duration: 1)) - } - } + } + } + .onAppear { + DispatchQueue.main.async { + scrollToFirstUnread(proxy) + scrolledToUnread = true + } + markAllRead() + } + .onChange(of: chatModel.reversedChatItems.first?.id) { _ in + scrollToBottom(proxy) + } + .onChange(of: keyboardVisible) { _ in + if keyboardVisible { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + scrollToBottom(proxy, animation: .easeInOut(duration: 1)) } } } .onTapGesture { hideKeyboard() } } } + .scaleEffect(x: 1, y: -1, anchor: .center) Spacer(minLength: 0) @@ -86,11 +78,19 @@ struct ChatView: View { ) .disabled(!chat.chatInfo.sendMsgEnabled) } + .padding(.top, 1) .navigationTitle(cInfo.chatViewName) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button { chatModel.chatId = nil } label: { + Button { + chatModel.chatId = nil + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + if chatModel.chatId == nil { + chatModel.reversedChatItems = [] + } + } + } label: { HStack(spacing: 4) { Image(systemName: "chevron.backward") Text("Chats", comment: "back button to return to chats list") @@ -178,76 +178,156 @@ struct ChatView: View { } } - private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View { - let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth) - .contextMenu { - if ci.isMsgContent() { - Button { - withAnimation { - if composeState.editing() { - composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) - } else { - composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) - } - } - } label: { Label("Reply", systemImage: "arrowshape.turn.up.left") } - Button { - var shareItems: [Any] = [ci.content.text] - if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { - shareItems.append(image) - } - showShareSheet(items: shareItems) - } label: { Label("Share", systemImage: "square.and.arrow.up") } - Button { - if case let .image(text, _) = ci.content.msgContent, - text == "", - let image = getLoadedImage(ci.file) { - UIPasteboard.general.image = image + private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { + if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { + if loadingItems || firstPage || !scrolledToUnread { return } + loadingItems = true + Task { + do { + let items = try await apiGetChatItems( + type: cInfo.chatType, + id: cInfo.apiId, + pagination: .before(chatItemId: firstItem.id, count: 50) + ) + await MainActor.run { + if items.count == 0 { + firstPage = true } else { - UIPasteboard.general.string = ci.content.text + chatModel.reversedChatItems.append(contentsOf: items.reversed()) } - } label: { Label("Copy", systemImage: "doc.on.doc") } - if case .image = ci.content.msgContent, - let image = getLoadedImage(ci.file) { - Button { - UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - } label: { Label("Save", systemImage: "square.and.arrow.down") } + loadingItems = false } - if ci.meta.editable { - Button { - withAnimation { - composeState = ComposeState(editingItem: ci) - } - } label: { Label("Edit", systemImage: "square.and.pencil") } - } - Button(role: .destructive) { - showDeleteMessage = true - deletingItem = ci - } label: { Label("Delete", systemImage: "trash") } - } else if ci.isDeletedContent() { - Button(role: .destructive) { - showDeleteMessage = true - deletingItem = ci - } label: { Label("Delete", systemImage: "trash") } + } catch let error { + logger.error("apiGetChat error: \(responseError(error))") + await MainActor.run { loadingItems = false } } } + } + } + + @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { + if case let .groupRcv(member) = ci.chatDir { + let prevItem = chatModel.getPrevChatItem(ci) + HStack(alignment: .top, spacing: 0) { + let showMember = prevItem == nil || showMemberImage(member, prevItem) + if showMember { + ProfileImage(imageStr: member.memberProfile.image) + .frame(width: memberImageSize, height: memberImageSize) + } else { + Rectangle().fill(.clear) + .frame(width: memberImageSize, height: memberImageSize) + } + chatItemWithMenu(ci, maxWidth, showMember: showMember).padding(.leading, 8) + } + .padding(.trailing) + .padding(.leading, 12) + } else { + chatItemWithMenu(ci, maxWidth).padding(.horizontal) + } + } + + private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View { + let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading + var menu: [UIAction] = [] + if ci.isMsgContent() { + menu.append(contentsOf: [ + UIAction( + title: NSLocalizedString("Reply", comment: "chat item action"), + image: UIImage(systemName: "arrowshape.turn.up.left") + ) { _ in + withAnimation { + if composeState.editing() { + composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) + } else { + composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + } + } + }, + UIAction( + title: NSLocalizedString("Share", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.up") + ) { _ in + var shareItems: [Any] = [ci.content.text] + if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { + shareItems.append(image) + } + showShareSheet(items: shareItems) + }, + UIAction( + title: NSLocalizedString("Copy", comment: "chat item action"), + image: UIImage(systemName: "doc.on.doc") + ) { _ in + if case let .image(text, _) = ci.content.msgContent, + text == "", + let image = getLoadedImage(ci.file) { + UIPasteboard.general.image = image + } else { + UIPasteboard.general.string = ci.content.text + } + } + ]) + if case .image = ci.content.msgContent, + let image = getLoadedImage(ci.file) { + menu.append( + UIAction( + title: NSLocalizedString("Save", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.down") + ) { _ in + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } + ) + } + if ci.meta.editable { + menu.append( + UIAction( + title: NSLocalizedString("Edit", comment: "chat item action"), + image: UIImage(systemName: "square.and.pencil") + ) { _ in + withAnimation { + composeState = ComposeState(editingItem: ci) + } + } + ) + } + menu.append( + UIAction( + title: NSLocalizedString("Delete", comment: "chat item action"), + image: UIImage(systemName: "trash"), + attributes: [.destructive] + ) { _ in + showDeleteMessage = true + deletingItem = ci + } + ) + } else if ci.isDeletedContent() { + menu.append( + UIAction( + title: NSLocalizedString("Delete", comment: "chat item action"), + image: UIImage(systemName: "trash"), + attributes: [.destructive] + ) { _ in + showDeleteMessage = true + deletingItem = ci + } + ) + } + + return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth) + .uiKitContextMenu(actions: menu) .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { deleteMessage(.cidmInternal) } - if let di = deletingItem { - if di.meta.editable { - Button("Delete for everyone",role: .destructive) { - deleteMessage(.cidmBroadcast) - } + if let di = deletingItem, di.meta.editable { + Button("Delete for everyone",role: .destructive) { + deleteMessage(.cidmBroadcast) } } } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) } - + private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { switch (prevItem?.chatDir) { case .groupSnd: return true @@ -261,15 +341,15 @@ struct ChatView: View { } func scrollToBottom_(_ proxy: ScrollViewProxy) { - if let id = chatModel.chatItems.last?.id { - proxy.scrollTo(id, anchor: .bottom) + if let id = chatModel.reversedChatItems.first?.id { + proxy.scrollTo(id, anchor: .top) } } // align first unread with the top or the last unread with bottom func scrollToFirstUnread(_ proxy: ScrollViewProxy) { - if let cItem = chatModel.chatItems.first(where: { $0.isRcvNew() }) { - proxy.scrollTo(cItem.id) + if let cItem = chatModel.reversedChatItems.last(where: { $0.isRcvNew() }) { + proxy.scrollTo(cItem.id, anchor: .bottom) } else { scrollToBottom_(proxy) } @@ -311,7 +391,7 @@ struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.chatId = "@1" - chatModel.chatItems = [ + chatModel.reversedChatItems = [ ChatItem.getSample(1, .directSnd, .now, "hello"), ChatItem.getSample(2, .directRcv, .now, "hi"), ChatItem.getSample(3, .directRcv, .now, "hi there"), diff --git a/apps/ios/Shared/Views/Helpers/ContextMenu.swift b/apps/ios/Shared/Views/Helpers/ContextMenu.swift new file mode 100644 index 0000000000..e2342c274b --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ContextMenu.swift @@ -0,0 +1,88 @@ +// +// ContextMenu2.swift +// SimpleX (iOS) +// +// Created by Evgeny on 09/08/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import UIKit +import SwiftUI + +extension View { + func uiKitContextMenu(title: String = "", actions: [UIAction]) -> some View { + self.overlay(Color(uiColor: .systemBackground)) + .overlay( + InteractionView(content: self, menu: UIMenu(title: title, children: actions)) + ) + } +} + +private struct InteractionConfig { + let content: Content + let menu: UIMenu +} + +private struct InteractionView: UIViewRepresentable { + let content: Content + let menu: UIMenu + + func makeUIView(context: Context) -> UIView { + let view = UIView() + view.backgroundColor = .clear + let hostView = UIHostingController(rootView: content) + hostView.view.translatesAutoresizingMaskIntoConstraints = false + let constraints = [ + hostView.view.topAnchor.constraint(equalTo: view.topAnchor), + hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor), + hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor) + ] + view.addSubview(hostView.view) + view.addConstraints(constraints) + let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator) + view.addInteraction(menuInteraction) + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UIContextMenuInteractionDelegate { + let parent: InteractionView + + init(_ parent: InteractionView) { + self.parent = parent + } + + func contextMenuInteraction( + _ interaction: UIContextMenuInteraction, + configurationForMenuAtLocation location: CGPoint + ) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration( + identifier: nil, + previewProvider: nil, + actionProvider: { [weak self] _ in + guard let self = self else { return nil } + return self.parent.menu + } + ) + } + + // func contextMenuInteraction( + // _ interaction: UIContextMenuInteraction, + // willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, + // animator: UIContextMenuInteractionCommitAnimating + // ) { + // animator.addCompletion { + // print("user tapped") + // } + // } + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 254da99fd9..8659b991bf 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; }; 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; + 5C00164028A1B87B0094D739 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5C00163F28A1B87B0094D739 /* Introspect */; }; + 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -194,6 +196,7 @@ 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = ""; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; + 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -323,6 +326,7 @@ buildActionMask = 2147483647; files = ( 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, + 5C00164028A1B87B0094D739 /* Introspect in Frameworks */, 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, ); @@ -452,6 +456,7 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */, 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */, 5C6BA666289BD954009B8ECC /* DismissSheets.swift */, + 5C00164328A26FBC0094D739 /* ContextMenu.swift */, ); path = Helpers; sourceTree = ""; @@ -685,6 +690,7 @@ name = "SimpleX (iOS)"; packageProductDependencies = ( 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */, + 5C00163F28A1B87B0094D739 /* Introspect */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -785,6 +791,7 @@ mainGroup = 5CA059BD279559F40002BEB4; packageReferences = ( 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */, + 5C00163E28A1B87B0094D739 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -861,6 +868,7 @@ 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, + 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */, @@ -1475,6 +1483,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 5C00163E28A1B87B0094D739 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.4; + }; + }; 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/twostraws/CodeScanner"; @@ -1486,6 +1502,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 5C00163F28A1B87B0094D739 /* Introspect */ = { + isa = XCSwiftPackageProductDependency; + package = 5C00163E28A1B87B0094D739 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = Introspect; + }; 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */ = { isa = XCSwiftPackageProductDependency; package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f1d1790ff..ded924d1c1 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,15 @@ "revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d", "version" : "2.1.1" } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect", + "state" : { + "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", + "version" : "0.1.4" + } } ], "version" : 2 diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index fcb886bf85..d51e8f911d 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -710,7 +710,7 @@ public struct ChatItem: Identifiable, Decodable { self.quotedItem = quotedItem self.file = file } - + public var chatDir: CIDirection public var meta: CIMeta public var content: CIContent @@ -718,9 +718,17 @@ public struct ChatItem: Identifiable, Decodable { public var quotedItem: CIQuote? public var file: CIFile? - public var id: Int64 { get { meta.itemId } } + public var viewTimestamp = Date.now - public var timestampText: Text { get { meta.timestampText } } + private enum CodingKeys: String, CodingKey { + case chatDir, meta, content, formattedText, quotedItem, file + } + + public var id: Int64 { meta.itemId } + + public var viewId: String { "\(meta.itemId) \(viewTimestamp.timeIntervalSince1970)" } + + public var timestampText: Text { meta.timestampText } public var text: String { get { @@ -855,6 +863,7 @@ public struct CIMeta: Decodable { var itemText: String public var itemStatus: CIStatus var createdAt: Date + var updatedAt: Date public var itemDeleted: Bool public var itemEdited: Bool public var editable: Bool @@ -868,6 +877,7 @@ public struct CIMeta: Decodable { itemText: text, itemStatus: status, createdAt: ts, + updatedAt: ts, itemDeleted: itemDeleted, itemEdited: itemEdited, editable: editable @@ -892,6 +902,17 @@ public enum CIStatus: Decodable { case sndError(agentError: AgentErrorType) case rcvNew case rcvRead + + var id: String { + switch self { + case .sndNew: return "sndNew" + case .sndSent: return "sndSent" + case .sndErrorAuth: return "sndErrorAuth" + case .sndError: return "sndError" + case .rcvNew: return "rcvNew" + case .rcvRead: return "rcvRead" + } + } } public enum CIDeleteMode: String, Decodable { From 0a2f7681d83d3a82044d3a142591d090c87e67f7 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 16 Aug 2022 15:08:15 +0300 Subject: [PATCH 18/89] Swipe to reply feature (#936) * Swipe to reply feature - ability to reply by swiping on a message from right-to-left or left-to-right - keyboard will be open automatically * Only one direction for swipe to reply action --- .../chat/simplex/app/views/chat/ChatView.kt | 58 ++++++++++++++++--- .../simplex/app/views/chat/SendMsgView.kt | 20 ++++++- .../simplex/app/views/helpers/Modifiers.kt | 27 +++++++++ 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 572bc6944a..572c4adb93 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -241,12 +241,18 @@ fun ChatLayout( modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = floatingButton.value, ) { contentPadding -> - BoxWithConstraints(Modifier.padding(contentPadding)) { - ChatItemsList( - user, chat, unreadCount, composeState, chatItems, - useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, - receiveFile, joinGroup, acceptCall, markRead, floatingButton - ) + CompositionLocalProvider( + // Makes horizontal and vertical scrolling to coexist nicely. + // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view + LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() + ) { + BoxWithConstraints(Modifier.padding(contentPadding)) { + ChatItemsList( + user, chat, unreadCount, composeState, chatItems, + useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, + receiveFile, joinGroup, acceptCall, markRead, floatingButton + ) + } } } } @@ -390,12 +396,33 @@ fun BoxWithConstraintsScope.ChatItemsList( val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } } LazyColumn(state = listState, reverseLayout = true) { itemsIndexed(reversedChatItems) { i, cItem -> + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } + val directions = setOf(DismissDirection.EndToStart) + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = directions, + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) + val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) + val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) + if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { + LaunchedEffect(Unit) { + scope.launch { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + } + } + } + } + if (chat.chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null val member = cItem.chatDir.groupMember val showMember = showMemberImage(member, prevItem) - Row(Modifier.padding(start = 8.dp, end = 66.dp)) { + Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) { if (showMember) { val contactId = member.memberContactId if (contactId == null) { @@ -420,7 +447,7 @@ fun BoxWithConstraintsScope.ChatItemsList( ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) } } else { - Box(Modifier.padding(start = 86.dp, end = 12.dp)) { + Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) { ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) } } @@ -430,7 +457,7 @@ fun BoxWithConstraintsScope.ChatItemsList( Modifier.padding( start = if (sent) 76.dp else 12.dp, end = if (sent) 12.dp else 76.dp, - ) + ).then(swipeableModifier) ) { ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) } @@ -634,6 +661,19 @@ private fun bottomEndFloatingButton( } } +private fun ViewConfiguration.bigTouchSlop(slop: Float = 80f) = object: ViewConfiguration { + override val longPressTimeoutMillis + get() = + this@bigTouchSlop.longPressTimeoutMillis + override val doubleTapTimeoutMillis + get() = + this@bigTouchSlop.doubleTapTimeoutMillis + override val doubleTapMinTimeMillis + get() = + this@bigTouchSlop.doubleTapMinTimeMillis + override val touchSlop: Float get() = slop +} + @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index b88f5230cc..e456f05766 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -12,11 +12,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization @@ -26,7 +28,9 @@ import chat.simplex.app.R import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.coroutines.delay +@OptIn(ExperimentalComposeUiApi::class) @Composable fun SendMsgView( composeState: MutableState, @@ -35,6 +39,16 @@ fun SendMsgView( textStyle: MutableState ) { val cs = composeState.value + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + LaunchedEffect(cs.contextItem) { + if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect + // In replying state + focusRequester.requestFocus() + delay(50) + keyboard?.show() + } + BasicTextField( value = cs.message, onValueChange = onMessageChange, @@ -44,7 +58,7 @@ fun SendMsgView( capitalization = KeyboardCapitalization.Sentences, autoCorrect = true ), - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.padding(vertical = 8.dp).focusRequester(focusRequester), cursorBrush = SolidColor(HighOrLowlight), decorationBox = { innerTextField -> Surface( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt index a8d9f0e435..f60d644a88 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt @@ -1,7 +1,15 @@ package chat.simplex.app.views.helpers +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.offset +import androidx.compose.material.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.roundToInt fun Modifier.badgeLayout() = layout { measurable, constraints -> @@ -15,3 +23,22 @@ fun Modifier.badgeLayout() = placeable.place((width - placeable.width) / 2, 0) } } + +@Composable +fun SwipeToDismissModifier( + state: DismissState, + directions: Set = setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd), + swipeDistance: Float, +): Modifier { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val anchors = mutableMapOf(0f to DismissValue.Default) + if (DismissDirection.StartToEnd in directions) anchors += swipeDistance to DismissValue.DismissedToEnd + if (DismissDirection.EndToStart in directions) anchors += -swipeDistance to DismissValue.DismissedToStart + return Modifier.swipeable( + state = state, + anchors = anchors, + thresholds = { _, _ -> FractionalThreshold(0.5f) }, + orientation = Orientation.Horizontal, + reverseDirection = isRtl, + ).offset { IntOffset(state.offset.value.roundToInt(), 0) } +} From 76bde53206db3c45699e56522e4c35cada6ad021 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 16 Aug 2022 13:13:29 +0100 Subject: [PATCH 19/89] ios: scroll buttons and unread counts (#937) * ios: scroll buttons and unread counts * floating buttons for unread counts * remove commented code * remove prints --- apps/ios/Shared/Model/ChatModel.swift | 82 +++++++-- apps/ios/Shared/Model/SimpleXAPI.swift | 6 +- apps/ios/Shared/SimpleXApp.swift | 5 - apps/ios/Shared/Views/Chat/ChatView.swift | 164 +++++++++++------- .../Views/ChatList/ChatPreviewView.swift | 6 +- 5 files changed, 179 insertions(+), 84 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 175e9c387e..046d18ce59 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -160,16 +160,6 @@ final class ChatModel: ObservableObject { // add to current chat if chatId == cInfo.id { withAnimation { reversedChatItems.insert(cItem, at: 0) } - if case .rcvNew = cItem.meta.itemStatus { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if self.chatId == cInfo.id { - Task { - await apiMarkChatItemRead(cInfo, cItem) - NtfManager.shared.decNtfBadgeCount() - } - } - } - } } } @@ -230,14 +220,44 @@ final class ChatModel: ObservableObject { } // update current chat if chatId == cInfo.id { - var i = 0 - while i < reversedChatItems.count { - if case .rcvNew = reversedChatItems[i].meta.itemStatus { - reversedChatItems[i].meta.itemStatus = .rcvRead - reversedChatItems[i].viewTimestamp = .now - } - i = i + 1 + markCurrentChatRead() + } + } + + private func markCurrentChatRead(fromIndex i: Int = 0) { + var j = i + while j < reversedChatItems.count { + if case .rcvNew = reversedChatItems[j].meta.itemStatus { + reversedChatItems[j].meta.itemStatus = .rcvRead + reversedChatItems[j].viewTimestamp = .now } + j += 1 + } + } + + func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) { + if let cItem = aboveItem { + if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) { + markCurrentChatRead(fromIndex: i) + if let chat = getChat(cInfo.id) { + var unreadBelow = 0 + var j = i - 1 + while j >= 0 { + if case .rcvNew = reversedChatItems[j].meta.itemStatus { + unreadBelow += 1 + } + j -= 1 + } + // update preview + let markedCount = chat.chatStats.unreadCount - unreadBelow + if markedCount > 0 { + NtfManager.shared.decNtfBadgeCount(by: markedCount) + chat.chatStats.unreadCount -= markedCount + } + } + } + } else { + markChatItemsRead(cInfo) } } @@ -312,6 +332,34 @@ final class ChatModel: ObservableObject { return false } } + + func unreadChatItemCounts(itemsInView: Set) -> UnreadChatItemCounts { + var i = 0 + var totalBelow = 0 + var unreadBelow = 0 + while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) { + totalBelow += 1 + if reversedChatItems[i].isRcvNew() { + unreadBelow += 1 + } + i += 1 + } + return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow) + } + + func topItemInView(itemsInView: Set) -> ChatItem? { + let maxIx = reversedChatItems.count - 1 + var i = 0 + let inView = { itemsInView.contains(self.reversedChatItems[$0].viewId) } + while i < maxIx && !inView(i) { i += 1 } + while i < maxIx && inView(i) { i += 1 } + return reversedChatItems[min(i - 1, maxIx)] + } +} + +struct UnreadChatItemCounts { + var totalBelow: Int + var unreadBelow: Int } final class Chat: ObservableObject, Identifiable { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 0bc3f9a5ee..ac841c5ebf 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -540,13 +540,13 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws { } } -func markChatRead(_ chat: Chat) async { +func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async { do { let minItemId = chat.chatStats.minUnreadItemId - let itemRange = (minItemId, chat.chatItems.last?.id ?? minItemId) + let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId) let cInfo = chat.chatInfo try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange) - DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo) } + DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) } } catch { logger.error("markChatRead apiChatRead error: \(responseError(error))") } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 8187de64ec..386ae0c431 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -116,11 +116,6 @@ struct SimpleXApp: App { if let id = chatModel.chatId, let chat = chatModel.getChat(id) { loadChat(chat: chat) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if chatModel.chatId == chat.id { - Task { await markChatRead(chat) } - } - } } if let chatId = chatModel.ntfContactRequest { chatModel.ntfContactRequest = nil diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 5522be3c44..324581f3fb 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -26,48 +26,19 @@ struct ChatView: View { @State private var tableView: UITableView? @State private var loadingItems = false @State private var firstPage = false - @State private var scrolledToUnread = false + @State private var itemsInView: Set = [] + @State private var scrollProxy: ScrollViewProxy? var body: some View { let cInfo = chat.chatInfo return VStack { - GeometryReader { g in - let maxWidth = - cInfo.chatType == .group - ? (g.size.width - 28) * 0.84 - 42 - : (g.size.width - 32) * 0.84 - ScrollViewReader { proxy in - ScrollView { - LazyVStack(spacing: 5) { - ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in - chatItemView(ci, maxWidth) - .scaleEffect(x: 1, y: -1, anchor: .center) - .onAppear { loadChatItems(cInfo, ci, proxy) } - } - } - } - .onAppear { - DispatchQueue.main.async { - scrollToFirstUnread(proxy) - scrolledToUnread = true - } - markAllRead() - } - .onChange(of: chatModel.reversedChatItems.first?.id) { _ in - scrollToBottom(proxy) - } - .onChange(of: keyboardVisible) { _ in - if keyboardVisible { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - scrollToBottom(proxy, animation: .easeInOut(duration: 1)) - } - } - } - .onTapGesture { hideKeyboard() } + ZStack(alignment: .trailing) { + chatItemsList(cInfo) + if let proxy = scrollProxy { + floatingButtons(proxy) } } - .scaleEffect(x: 1, y: -1, anchor: .center) Spacer(minLength: 0) @@ -154,6 +125,98 @@ struct ChatView: View { .navigationBarBackButtonHidden(true) } + private func chatItemsList(_ cInfo: ChatInfo) -> some View { + GeometryReader { g in + ScrollViewReader { proxy in + ScrollView { + let maxWidth = + cInfo.chatType == .group + ? (g.size.width - 28) * 0.84 - 42 + : (g.size.width - 32) * 0.84 + LazyVStack(spacing: 5) { + ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in + chatItemView(ci, maxWidth) + .scaleEffect(x: 1, y: -1, anchor: .center) + .onAppear { + itemsInView.insert(ci.viewId) + loadChatItems(cInfo, ci, proxy) + if ci.isRcvNew() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { + Task { + await apiMarkChatItemRead(cInfo, ci) + NtfManager.shared.decNtfBadgeCount() + } + } + } + } + } + .onDisappear { + itemsInView.remove(ci.viewId) + } + } + } + } + .onAppear { + scrollProxy = proxy + } + .onTapGesture { hideKeyboard() } + } + } + .scaleEffect(x: 1, y: -1, anchor: .center) + } + + private func floatingButtons(_ proxy: ScrollViewProxy) -> some View { + let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView) + return VStack { + let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow + if unreadAbove > 0 { + circleButton { + unreadCountText(unreadAbove) + .font(.callout) + .foregroundColor(.accentColor) + } + .onTapGesture { scrollUp(proxy) } + .contextMenu { + Button { + if let ci = chatModel.topItemInView(itemsInView: itemsInView) { + Task { + await markChatRead(chat, aboveItem: ci) + } + } + } label: { + Label("Mark read", systemImage: "checkmark") + } + } + } + Spacer() + if counts.unreadBelow > 0 { + circleButton { + unreadCountText(counts.unreadBelow) + .font(.callout) + .foregroundColor(.accentColor) + } + .onTapGesture { scrollToBottom(proxy) } + } else if counts.totalBelow > 16 { + circleButton { + Image(systemName: "chevron.down") + .foregroundColor(.accentColor) + } + .onTapGesture { scrollToBottom(proxy) } + } + } + .padding() + } + + private func circleButton(_ content: @escaping () -> Content) -> some View { + ZStack { + Circle() + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground)) + .frame(width: 44, height: 44) + content() + } + } + private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View { Button { CallController.shared.startCall(contact, media) @@ -180,7 +243,7 @@ struct ChatView: View { private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { - if loadingItems || firstPage || !scrolledToUnread { return } + if loadingItems || firstPage { return } loadingItems = true Task { do { @@ -336,34 +399,19 @@ struct ChatView: View { } } - func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) { - withAnimation(animation) { scrollToBottom_(proxy) } - } - - func scrollToBottom_(_ proxy: ScrollViewProxy) { - if let id = chatModel.reversedChatItems.first?.id { - proxy.scrollTo(id, anchor: .top) + private func scrollToBottom(_ proxy: ScrollViewProxy) { + if let ci = chatModel.reversedChatItems.first { + withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } } } - // align first unread with the top or the last unread with bottom - func scrollToFirstUnread(_ proxy: ScrollViewProxy) { - if let cItem = chatModel.reversedChatItems.last(where: { $0.isRcvNew() }) { - proxy.scrollTo(cItem.id, anchor: .bottom) - } else { - scrollToBottom_(proxy) + private func scrollUp(_ proxy: ScrollViewProxy) { + if let ci = chatModel.topItemInView(itemsInView: itemsInView) { + withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } } } - func markAllRead() { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if chatModel.chatId == chat.id { - Task { await markChatRead(chat) } - } - } - } - - func deleteMessage(_ mode: CIDeleteMode) { + private func deleteMessage(_ mode: CIDeleteMode) { logger.debug("ChatView deleteMessage") Task { logger.debug("ChatView deleteMessage: in Task") diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index efda4c2341..84819a92b6 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -108,7 +108,7 @@ struct ChatPreviewView: View { .padding(.trailing, 36) .padding(.bottom, 4) if unread > 0 { - Text(unread > 999 ? "\(unread / 1000)k" : "\(unread)") + unreadCountText(unread) .font(.caption) .foregroundColor(.white) .padding(.horizontal, 4) @@ -170,6 +170,10 @@ struct ChatPreviewView: View { } } +func unreadCountText(_ n: Int) -> Text { + Text(n > 999 ? "\(n / 1000)k" : "\(n)") +} + struct ChatPreviewView_Previews: PreviewProvider { static var previews: some View { Group { From e9cddd6ca32f88d88e09339370b7a5353409acbf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:56:21 +0100 Subject: [PATCH 20/89] core: add search parameter name to /_get chat api (#939) --- src/Simplex/Chat.hs | 4 ++-- tests/ChatTests.hs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 477b71a16d..79e7d33325 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2463,7 +2463,7 @@ chatCommandP = "/_db import " *> (APIImportArchive <$> jsonP), "/_db delete" $> APIDeleteStorage, "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), - "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (A.space *> search)), + "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP), "/_get items count=" *> (APIGetChatItems <$> A.decimal), "/_send " *> (APISendMessage <$> chatRefP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> msgContentP), @@ -2592,7 +2592,7 @@ chatCommandP = n <- (A.space *> A.takeByteString) <|> pure "" pure $ if B.null n then name else safeDecodeUtf8 n filePath = T.unpack . safeDecodeUtf8 <$> A.takeByteString - search = T.unpack . safeDecodeUtf8 <$> A.takeByteString + searchP = T.unpack . safeDecodeUtf8 <$> (" search=" *> A.takeByteString) memberRole = (" owner" $> GROwner) <|> (" admin" $> GRAdmin) diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 046ee25cf0..c44a49bfca 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -212,7 +212,7 @@ testAddContact = versionTestMatrix2 runTestAddContact alice #$> ("/_get chat @2 after=1 count=100", chat, [(0, "hello there"), (0, "how are you?")]) alice #$> ("/_get chat @2 before=2 count=100", chat, [(1, "hello there 🙂")]) -- search - alice #$> ("/_get chat @2 count=100 ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) + alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages alice #$> ("/_read chat @2 from=1 to=100", id, "ok") bob #$> ("/_read chat @2 from=1 to=100", id, "ok") @@ -479,7 +479,7 @@ testGroupShared alice bob cath checkMessages = do -- so we take into account group event items as well as sent group invitations in direct chats alice #$> ("/_get chat #1 after=5 count=100", chat, [(0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 before=7 count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) - alice #$> ("/_get chat #1 count=100 team", chat, [(0, "hey team")]) + alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")]) bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] bob #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] From ae6fae5ced09abe3b0590f5d148a5edb5050f59f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 16 Aug 2022 21:45:03 +0100 Subject: [PATCH 21/89] core: update simplexmq (servers migration) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 9f07946f90..187a347990 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 04377962328b0b9e722d502f5cab91a189f43dbc + tag: cfab5a18d3f053b7ccbd61c00d26b3115347f04d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index f697c34804..88ad06c0a5 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."04377962328b0b9e722d502f5cab91a189f43dbc" = "0y3mgg7li5xg3dk4cfh46gvaqrg34rxskv9qp2jbjxh7p6iaj670"; + "https://github.com/simplex-chat/simplexmq.git"."cfab5a18d3f053b7ccbd61c00d26b3115347f04d" = "1qqg87b4jdkd6drknk95931g3iq28x22swhgyfqmhi8f1m5h8rk0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index cd2be0e807..86fe4333dc 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 04377962328b0b9e722d502f5cab91a189f43dbc + commit: cfab5a18d3f053b7ccbd61c00d26b3115347f04d # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From fd2783944254855265a6e8feecba89e15cad91be Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:43:18 +0100 Subject: [PATCH 22/89] ios: search in chat (#938) * ios: search in chat * update libraries and search API * layout --- apps/ios/Shared/Model/SimpleXAPI.swift | 12 +-- apps/ios/Shared/Views/Chat/ChatView.swift | 104 +++++++++++++++++---- apps/ios/SimpleX.xcodeproj/project.pbxproj | 48 ++++------ apps/ios/SimpleXChat/APITypes.swift | 5 +- 4 files changed, 115 insertions(+), 54 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ac841c5ebf..bdd037e769 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -185,22 +185,22 @@ func apiGetChats() throws -> [ChatData] { throw r } -func apiGetChat(type: ChatType, id: Int64) throws -> Chat { - let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50))) +func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat { + let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search)) if case let .apiChat(chat) = r { return Chat.init(chat) } throw r } -func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination) async throws -> [ChatItem] { - let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination)) +func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] { + let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search)) if case let .apiChat(chat) = r { return chat.chatItems } throw r } -func loadChat(chat: Chat) { +func loadChat(chat: Chat, search: String = "") { do { let cInfo = chat.chatInfo - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId) + let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) let m = ChatModel.shared m.updateChatInfo(chat.chatInfo) m.reversedChatItems = chat.chatItems.reversed() diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 324581f3fb..d09e2ebd29 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -28,13 +28,19 @@ struct ChatView: View { @State private var firstPage = false @State private var itemsInView: Set = [] @State private var scrollProxy: ScrollViewProxy? + @State private var searchMode = false + @State private var searchText: String = "" + @FocusState private var searchFocussed var body: some View { let cInfo = chat.chatInfo - - return VStack { + return VStack(spacing: 0) { + if searchMode { + searchToolbar() + Divider() + } ZStack(alignment: .trailing) { - chatItemsList(cInfo) + chatItemsList() if let proxy = scrollProxy { floatingButtons(proxy) } @@ -47,11 +53,12 @@ struct ChatView: View { composeState: $composeState, keyboardVisible: $keyboardVisible ) - .disabled(!chat.chatInfo.sendMsgEnabled) + .disabled(!cInfo.sendMsgEnabled) } .padding(.top, 1) .navigationTitle(cInfo.chatViewName) .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { @@ -62,10 +69,7 @@ struct ChatView: View { } } } label: { - HStack(spacing: 4) { - Image(systemName: "chevron.backward") - Text("Chats", comment: "back button to return to chats list") - } + Image(systemName: "chevron.backward") } } ToolbarItem(placement: .principal) { @@ -108,25 +112,75 @@ struct ChatView: View { case let .direct(contact): HStack { callButton(contact, .audio, imageName: "phone") - callButton(contact, .video, imageName: "video") + Menu { + Button { + CallController.shared.startCall(contact, .video) + } label: { + Label("Video call", systemImage: "video") + } + searchButton() + } label: { + Image(systemName: "ellipsis") + } } case let .group(groupInfo): - if groupInfo.canAddMembers { - addMembersButton() - .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) - } + HStack { + if groupInfo.canAddMembers { + addMembersButton() + .sheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + } + } + Menu { + searchButton() + } label: { + Image(systemName: "ellipsis") + } } default: EmptyView() } } } - .navigationBarBackButtonHidden(true) } - private func chatItemsList(_ cInfo: ChatInfo) -> some View { - GeometryReader { g in + private func searchToolbar() -> some View { + HStack { + HStack { + Image(systemName: "magnifyingglass") + TextField("Search", text: $searchText) + .focused($searchFocussed) + .foregroundColor(.primary) + .frame(maxWidth: .infinity) + + Button { + searchText = "" + } label: { + Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1) + } + } + .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)) + .foregroundColor(.secondary) + .background(Color(.secondarySystemBackground)) + .cornerRadius(10.0) + + Button ("Cancel") { + searchText = "" + searchMode = false + searchFocussed = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + chatModel.reversedChatItems = [] + loadChat(chat: chat) + } + } + } + .padding(.horizontal) + .padding(.vertical, 8) + } + + private func chatItemsList() -> some View { + let cInfo = chat.chatInfo + return GeometryReader { g in ScrollViewReader { proxy in ScrollView { let maxWidth = @@ -161,6 +215,9 @@ struct ChatView: View { scrollProxy = proxy } .onTapGesture { hideKeyboard() } + .onChange(of: searchText) { _ in + loadChat(chat: chat, search: searchText) + } } } .scaleEffect(x: 1, y: -1, anchor: .center) @@ -225,6 +282,16 @@ struct ChatView: View { } } + private func searchButton() -> some View { + Button { + searchMode = true + searchFocussed = true + searchText = "" + } label: { + Label("Search", systemImage: "magnifyingglass") + } + } + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { @@ -250,7 +317,8 @@ struct ChatView: View { let items = try await apiGetChatItems( type: cInfo.chatType, id: cInfo.apiId, - pagination: .before(chatItemId: firstItem.id, count: 50) + pagination: .before(chatItemId: firstItem.id, count: 50), + search: searchText ) await MainActor.run { if items.count == 0 { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8659b991bf..cab6545152 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -12,8 +12,12 @@ 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; }; 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; - 5C00164028A1B87B0094D739 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5C00163F28A1B87B0094D739 /* Introspect */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; + 5C00164C28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */; }; + 5C00164D28ACEA380094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164828ACEA380094D739 /* libgmp.a */; }; + 5C00164E28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */; }; + 5C00164F28ACEA380094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164A28ACEA380094D739 /* libffi.a */; }; + 5C00165028ACEA380094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164B28ACEA380094D739 /* libgmpxx.a */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -54,11 +58,6 @@ 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; }; - 5C9C2D9F28929B6900CC63B1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9A28929B6900CC63B1 /* libffi.a */; }; - 5C9C2DA028929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */; }; - 5C9C2DA128929B6900CC63B1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9C28929B6900CC63B1 /* libgmp.a */; }; - 5C9C2DA228929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */; }; - 5C9C2DA328929B6900CC63B1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */; }; 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */; }; 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */; }; 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */; }; @@ -197,6 +196,11 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; + 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a"; sourceTree = ""; }; + 5C00164828ACEA380094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a"; sourceTree = ""; }; + 5C00164A28ACEA380094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C00164B28ACEA380094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -239,11 +243,6 @@ 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = ""; }; - 5C9C2D9A28929B6900CC63B1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a"; sourceTree = ""; }; - 5C9C2D9C28929B6900CC63B1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a"; sourceTree = ""; }; - 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = ""; }; 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedNetworkSettings.swift; sourceTree = ""; }; 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = ""; }; @@ -326,7 +325,6 @@ buildActionMask = 2147483647; files = ( 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, - 5C00164028A1B87B0094D739 /* Introspect in Frameworks */, 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, ); @@ -351,13 +349,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C9C2DA028929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a in Frameworks */, - 5C9C2DA128929B6900CC63B1 /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C9C2DA228929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a in Frameworks */, - 5C9C2D9F28929B6900CC63B1 /* libffi.a in Frameworks */, + 5C00164F28ACEA380094D739 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C9C2DA328929B6900CC63B1 /* libgmpxx.a in Frameworks */, + 5C00164E28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a in Frameworks */, + 5C00164D28ACEA380094D739 /* libgmp.a in Frameworks */, + 5C00164C28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a in Frameworks */, + 5C00165028ACEA380094D739 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -412,11 +410,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C9C2D9A28929B6900CC63B1 /* libffi.a */, - 5C9C2D9C28929B6900CC63B1 /* libgmp.a */, - 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */, - 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */, - 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */, + 5C00164A28ACEA380094D739 /* libffi.a */, + 5C00164828ACEA380094D739 /* libgmp.a */, + 5C00164B28ACEA380094D739 /* libgmpxx.a */, + 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */, + 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */, ); path = Libraries; sourceTree = ""; @@ -690,7 +688,6 @@ name = "SimpleX (iOS)"; packageProductDependencies = ( 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */, - 5C00163F28A1B87B0094D739 /* Introspect */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1502,11 +1499,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 5C00163F28A1B87B0094D739 /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = 5C00163E28A1B87B0094D739 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */ = { isa = XCSwiftPackageProductDependency; package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index f34a12524e..b0bc469594 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -24,7 +24,7 @@ public enum ChatCommand { case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage case apiGetChats - case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination) + case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) @@ -85,7 +85,8 @@ public enum ChatCommand { case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" case .apiGetChats: return "/_get chats pcc=on" - case let .apiGetChat(type, id, pagination): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + + (search == "" ? "" : " search=\(search)") case let .apiSendMessage(type, id, file, quotedItemId, mc): let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc)) return "/_send \(ref(type, id)) json \(msg)" From cad1ad87a81099113ec54f62b97aeef59860567e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:58:57 +0100 Subject: [PATCH 23/89] android: search (#940) * Search in chat messages * align api with core/swift * EOLs * DefaultTopAppBar changes Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 8 +- .../java/chat/simplex/app/ui/theme/Color.kt | 4 +- .../chat/simplex/app/views/chat/ChatView.kt | 172 ++++++++++++------ .../app/views/chat/item/ChatItemView.kt | 3 +- .../app/views/chatlist/ChatListNavLinkView.kt | 11 +- .../app/views/chatlist/ChatListView.kt | 82 ++++----- .../app/views/helpers/DefaultTopAppBar.kt | 124 +++++++++++++ .../app/views/helpers/SearchTextField.kt | 97 ++++++++++ .../app/src/main/res/values-ru/strings.xml | 1 + .../app/src/main/res/values/strings.xml | 1 + 10 files changed, 389 insertions(+), 114 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 7cf7053630..ada4d57020 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -314,8 +314,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("failed getting the list of chats: ${r.responseType} ${r.details}") } - suspend fun apiGetChat(type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT)): Chat? { - val r = sendCmd(CC.ApiGetChat(type, id, pagination)) + suspend fun apiGetChat(type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? { + val r = sendCmd(CC.ApiGetChat(type, id, pagination, search)) if (r is CR.ApiChat ) return r.chat Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") return null @@ -1115,7 +1115,7 @@ sealed class CC { class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() class ApiGetChats: CC() - class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination): CC() + class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() @@ -1166,7 +1166,7 @@ sealed class CC { is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" is ApiGetChats -> "/_get chats pcc=on" - is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " $search") is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}" is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 8f02fee808..fde270bb38 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -15,8 +15,8 @@ val DarkGray = Color(43, 44, 46, 255) val HighOrLowlight = Color(139, 135, 134, 255) val MessagePreviewDark = Color(179, 175, 174, 255) val MessagePreviewLight = Color(49, 45, 44, 255) -val ToolbarLight = Color(220, 220, 220, 20) -val ToolbarDark = Color(80, 80, 80, 20) +val ToolbarLight = Color(220, 220, 220, 12) +val ToolbarDark = Color(80, 80, 80, 12) val SettingsBackgroundLight = Color(220, 216, 215, 90) val SettingsSecondaryLight = Color(200, 196, 195, 90) val GroupDark = Color(80, 80, 80, 60) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 572c4adb93..fd297bcfbd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -2,7 +2,6 @@ package chat.simplex.app.views.chat import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.annotation.StringRes import androidx.compose.foundation.* import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* @@ -12,6 +11,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver @@ -20,10 +20,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* @@ -34,8 +35,10 @@ import chat.simplex.app.views.call.* import chat.simplex.app.views.chat.group.AddGroupMembersView import chat.simplex.app.views.chat.group.GroupChatInfoView import chat.simplex.app.views.chat.item.ChatItemView +import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.helpers.AppBarHeight import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding import kotlinx.coroutines.* @@ -45,6 +48,7 @@ import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel) { var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } + val searchText = remember { mutableStateOf("") } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) } @@ -83,6 +87,7 @@ fun ChatView(chatModel: ChatModel) { scope, attachmentBottomSheetState, chatModel.chatItems, + searchText, useLinkPreviews = useLinkPreviews, back = { chatModel.chatId.value = null }, info = { @@ -127,7 +132,9 @@ fun ChatView(chatModel: ChatModel) { val c = chatModel.getChat(cInfo.id) val firstId = chatModel.chatItems.firstOrNull()?.id if (c != null && firstId != null) { - withApi { apiLoadPrevMessages(firstId, c.chatInfo, chatModel) } + withApi { + apiLoadPrevMessages(c.chatInfo, chatModel, firstId, searchText.value) + } } }, deleteMessage = { itemId, mode -> @@ -187,6 +194,14 @@ fun ChatView(chatModel: ChatModel) { range ) } + }, + onSearchValueChanged = { value -> + if (searchText.value == value) return@ChatLayout + val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout + withApi { + apiFindMessages(c.chatInfo, chatModel, value) + searchText.value = value + } } ) } @@ -203,6 +218,7 @@ fun ChatLayout( scope: CoroutineScope, attachmentBottomSheetState: ModalBottomSheetState, chatItems: List, + searchValue: State, useLinkPreviews: Boolean, back: () -> Unit, info: () -> Unit, @@ -215,6 +231,7 @@ fun ChatLayout( acceptCall: (Contact) -> Unit, addMembers: (GroupInfo) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + onSearchValueChanged: (String) -> Unit, ) { Surface( Modifier @@ -236,7 +253,7 @@ fun ChatLayout( ) { val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } Scaffold( - topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers) }, + topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers, onSearchValueChanged) }, bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = floatingButton.value, @@ -248,7 +265,7 @@ fun ChatLayout( ) { BoxWithConstraints(Modifier.padding(contentPadding)) { ChatItemsList( - user, chat, unreadCount, composeState, chatItems, + user, chat, unreadCount, composeState, chatItems, searchValue, useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, receiveFile, joinGroup, acceptCall, markRead, floatingButton ) @@ -266,54 +283,79 @@ fun ChatInfoToolbar( back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, - addMembers: (GroupInfo) -> Unit + addMembers: (GroupInfo) -> Unit, + onSearchValueChanged: (String) -> Unit, ) { - @Composable fun toolbarButton(icon: ImageVector, @StringRes textId: Int, modifier: Modifier = Modifier.padding(0.dp), onClick: () -> Unit) { - IconButton(onClick, modifier = modifier) { - Icon(icon, stringResource(textId), tint = MaterialTheme.colors.primary) + var showMenu by remember { mutableStateOf(false) } + var showSearch by remember { mutableStateOf(false) } + val onBackClicked = { + if (!showSearch) { + back() + } else { + onSearchValueChanged("") + showSearch = false } } - Column { - Box( - Modifier - .fillMaxWidth() - .height(52.dp) - .background(if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(horizontal = 4.dp), - contentAlignment = Alignment.CenterStart, - ) { - val cInfo = chat.chatInfo - toolbarButton(Icons.Outlined.ArrowBackIos, R.string.back, onClick = back) - if (cInfo is ChatInfo.Direct) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { - Box(Modifier.width(85.dp), contentAlignment = Alignment.CenterStart) { - toolbarButton(Icons.Outlined.Phone, R.string.icon_descr_audio_call) { - startCall(CallMediaType.Audio) - } - } - toolbarButton(Icons.Outlined.Videocam, R.string.icon_descr_video_call) { - startCall(CallMediaType.Video) - } - } - } else if (cInfo is ChatInfo.Group) { - if (cInfo.groupInfo.canAddMembers) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { - toolbarButton(Icons.Outlined.PersonAdd, R.string.icon_descr_add_members) { - addMembers(cInfo.groupInfo) - } - } - } - } - Box( - Modifier - .padding(horizontal = 80.dp).fillMaxWidth() - .clickable(onClick = info), - contentAlignment = Alignment.Center - ) { - ChatInfoToolbarTitle(cInfo) + BackHandler(onBack = onBackClicked) + val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() + val menuItems = arrayListOf<@Composable () -> Unit>() + menuItems.add { + ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), Icons.Outlined.Search, onClick = { + showMenu = false + showSearch = true + }) + } + + if (chat.chatInfo is ChatInfo.Direct) { + barButtons.add { + IconButton({ + showMenu = false + startCall(CallMediaType.Audio) + }) { + Icon(Icons.Outlined.Phone, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary) } } - Divider() + menuItems.add { + ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), Icons.Outlined.Videocam, onClick = { + showMenu = false + startCall(CallMediaType.Video) + }) + } + } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { + barButtons.add { + IconButton({ + showMenu = false + addMembers(chat.chatInfo.groupInfo) + }) { + Icon(Icons.Outlined.PersonAdd, stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary) + } + } + } + barButtons.add { + IconButton({ showMenu = true }) { + Icon(Icons.Default.MoreVert, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary) + } + } + + DefaultTopAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + title = { ChatInfoToolbarTitle(chat.chatInfo) }, + onTitleClick = info, + showSearch = showSearch, + onSearchValueChanged = onSearchValueChanged, + buttons = barButtons + ) + + Divider(Modifier.padding(top = AppBarHeight)) + + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) { + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + Modifier.widthIn(min = 220.dp) + ) { + menuItems.forEach { it() } + } } } @@ -361,6 +403,7 @@ fun BoxWithConstraintsScope.ChatItemsList( unreadCount: State, composeState: MutableState, chatItems: List, + searchValue: State, useLinkPreviews: Boolean, openDirectChat: (Long) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, @@ -379,13 +422,19 @@ fun BoxWithConstraintsScope.ChatItemsList( // Helps to scroll to bottom after moving from Group to Direct chat // and prevents scrolling to bottom on orientation change var shouldAutoScroll by rememberSaveable { mutableStateOf(true) } - LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType) { + LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType, shouldAutoScroll) { if (shouldAutoScroll && listState.firstVisibleItemIndex != 0) { scope.launch { listState.scrollToItem(0) } } // Don't autoscroll next time until it will be needed shouldAutoScroll = false } + // Scroll to bottom when search value changes from something to nothing and back + LaunchedEffect(searchValue.value.isEmpty()) { + if (listState.firstVisibleItemIndex != 0) { + scope.launch { listState.scrollToItem(0) } + } + } PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, chat, chatItems) { c -> loadPrevMessages(c.chatInfo) @@ -473,7 +522,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } - FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, markRead, floatingButton, listState) + FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, floatingButton, listState) } @Composable @@ -481,24 +530,26 @@ fun BoxWithConstraintsScope.FloatingButtons( chatItems: List, unreadCount: State, minUnreadItemId: Long, + searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, floatingButton: MutableState<@Composable () -> Unit>, listState: LazyListState ) { val scope = rememberCoroutineScope() + val bottomUnreadCount by remember { + derivedStateOf { + val from = chatItems.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex + if (chatItems.size <= from || from < 0) return@derivedStateOf 0 - val bottomUnreadCount by remember { derivedStateOf { - chatItems.subList( - chatItems.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex, - chatItems.size - ).count { it.isRcvNew } } + chatItems.subList(from, chatItems.size).count { it.isRcvNew } + } } val firstItemIsVisible by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { - val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible + val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible floatingButton.value = bottomEndFloatingButton( bottomUnreadCount, @@ -512,7 +563,8 @@ fun BoxWithConstraintsScope.FloatingButtons( } ) } - + // Don't show top FAB if is in search + if (searchValue.value.isNotEmpty()) return val fabSize = 56.dp val topUnreadCount by remember { derivedStateOf { unreadCount.value - bottomUnreadCount } @@ -702,6 +754,7 @@ fun PreviewChatLayout() { ) ) val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } + val searchValue = remember { mutableStateOf("") } ChatLayout( user = User.sampleData, chat = Chat( @@ -716,6 +769,7 @@ fun PreviewChatLayout() { scope = rememberCoroutineScope(), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), chatItems = chatItems, + searchValue, useLinkPreviews = true, back = {}, info = {}, @@ -728,6 +782,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, addMembers = { _ -> }, markRead = { _, _ -> }, + onSearchValueChanged = {}, ) } } @@ -755,6 +810,7 @@ fun PreviewGroupChatLayout() { ) ) val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } + val searchValue = remember { mutableStateOf("") } ChatLayout( user = User.sampleData, chat = Chat( @@ -769,6 +825,7 @@ fun PreviewGroupChatLayout() { scope = rememberCoroutineScope(), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), chatItems = chatItems, + searchValue, useLinkPreviews = true, back = {}, info = {}, @@ -781,6 +838,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, addMembers = { _ -> }, markRead = { _, _ -> }, + onSearchValueChanged = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 19732416a4..7392526c39 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -164,7 +164,8 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo text, modifier = Modifier .fillMaxWidth() - .weight(1F), + .weight(1F) + .padding(end = 15.dp), color = color ) Icon(icon, text, tint = color) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 6e3ec9f3a5..ade4c9abe8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -96,11 +96,16 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { } } -suspend fun apiLoadPrevMessages(beforeChatItemId: Long, chatInfo: ChatInfo, chatModel: ChatModel) { +suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) { val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) - val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination) ?: return + val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return + chatModel.chatItems.addAll(0, chat.chatItems) +} + +suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: String) { + val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, search = search) ?: return + chatModel.chatItems.clear() chatModel.chatItems.addAll(0, chat.chatItems) - chatModel.chatId.value = chatInfo.id } suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index aeff3cc29c..a6f1029f34 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -10,7 +10,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource @@ -18,10 +17,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.ToolbarDark -import chat.simplex.app.ui.theme.ToolbarLight -import chat.simplex.app.views.helpers.AlertManager -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.NewChatSheet import chat.simplex.app.views.onboarding.MakeConnection import chat.simplex.app.views.usersettings.SettingsView @@ -72,6 +68,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() } BottomSheetScaffold( + topBar = { ChatListToolbar(scaffoldCtrl, stopped) }, scaffoldState = scaffoldCtrl.state, drawerContent = { SettingsView(chatModel, setPerformLA) }, sheetPeekHeight = 0.dp, @@ -84,8 +81,6 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: .fillMaxSize() .background(MaterialTheme.colors.background) ) { - ChatListToolbar(scaffoldCtrl, stopped) - Divider() if (chatModel.chats.isNotEmpty()) { ChatList(chatModel) } else { @@ -106,49 +101,42 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: @Composable fun ChatListToolbar(scaffoldCtrl: ScaffoldController, stopped: Boolean) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(horizontal = 8.dp) - ) { - IconButton(onClick = { scaffoldCtrl.toggleDrawer() }) { - Icon( - Icons.Outlined.Menu, - stringResource(R.string.icon_descr_settings), - tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(10.dp) + DefaultTopAppBar( + navigationButton = { NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } }, + title = { + Text( + stringResource(R.string.your_chats), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, ) - } - Text( - stringResource(R.string.your_chats), - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(5.dp) - ) - if (!stopped) { - IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { - Icon( - Icons.Outlined.AddCircle, - stringResource(R.string.add_contact), - tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(10.dp).size(26.dp) - ) - } - } else { - IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication), generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) { - Icon( - Icons.Filled.Report, - generalGetString(R.string.chat_is_stopped_indication), - tint = Color.Red, - modifier = Modifier.padding(10.dp) - ) + }, + onTitleClick = null, + showSearch = false, + onSearchValueChanged = {}, + buttons = listOf{ + if (!stopped) { + IconButton(onClick = { scaffoldCtrl.toggleSheet() }) { + Icon( + Icons.Outlined.AddCircle, + stringResource(R.string.add_contact), + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(10.dp).size(26.dp) + ) + } + } else { + IconButton(onClick = { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_is_stopped_indication), + generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)) }) { + Icon( + Icons.Filled.Report, + generalGetString(R.string.chat_is_stopped_indication), + tint = Color.Red, + modifier = Modifier.padding(10.dp) + ) + } } } - } + ) + Divider() } @Composable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt new file mode 100644 index 0000000000..1e6c37902a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt @@ -0,0 +1,124 @@ +package chat.simplex.app.views.helpers + +import chat.simplex.app.R +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBackIos +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.* +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.app.ui.theme.ToolbarDark +import chat.simplex.app.ui.theme.ToolbarLight + +@Composable +fun DefaultTopAppBar( + navigationButton: @Composable RowScope.() -> Unit, + title: @Composable () -> Unit, + onTitleClick: (() -> Unit)? = null, + showSearch: Boolean, + onSearchValueChanged: (String) -> Unit, + buttons: List<@Composable RowScope.() -> Unit> = emptyList(), +) { + // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier + val modifier = if (!showSearch) { + Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { }) + } else Modifier + + TopAppBar( + modifier = modifier, + title = { + if (!showSearch) { + title() + } else { + SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged) + } + }, + backgroundColor = if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight, + navigationIcon = navigationButton, + buttons = if (!showSearch) buttons else emptyList(), + centered = !showSearch + ) +} + +@Composable +fun NavigationButtonBack(onButtonClicked: () -> Unit) { + IconButton(onButtonClicked) { + Icon( + Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +fun NavigationButtonMenu(onButtonClicked: () -> Unit) { + IconButton(onClick = onButtonClicked) { + Icon( + Icons.Outlined.Menu, + stringResource(R.string.icon_descr_settings), + tint = MaterialTheme.colors.primary, + ) + } +} + +@Composable +private fun TopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: List<@Composable RowScope.() -> Unit> = emptyList(), + backgroundColor: Color = MaterialTheme.colors.primarySurface, + centered: Boolean, +) { + Box( + modifier + .fillMaxWidth() + .height(AppBarHeight) + .background(backgroundColor) + .padding(horizontal = 4.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (navigationIcon != null) { + Row( + Modifier + .fillMaxHeight() + .width(TitleInsetWithIcon - AppBarHorizontalPadding), + verticalAlignment = Alignment.CenterVertically, + content = navigationIcon + ) + } + + Row( + Modifier + .fillMaxHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + buttons.forEach { it() } + } + val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon + val endPadding = (buttons.size * 50f).dp + Box( + Modifier + .fillMaxWidth() + .padding( + start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding, + end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding + ), + contentAlignment = Alignment.Center + ) { + title() + } + } +} + +val AppBarHeight = 56.dp +private val AppBarHorizontalPadding = 4.dp +private val TitleInsetWithoutIcon = 16.dp - AppBarHorizontalPadding +private val TitleInsetWithIcon = 72.dp diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt new file mode 100644 index 0000000000..b2fc5d6da0 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt @@ -0,0 +1,97 @@ +package chat.simplex.app.views.helpers + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.foundation.text.* +import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.indicatorLine +import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import kotlinx.coroutines.delay + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) { + var searchText by remember { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + delay(200) + keyboard?.show() + } + + val enabled = true + val colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Unspecified, + textColor = MaterialTheme.colors.onBackground, + focusedIndicatorColor = Color.Unspecified, + unfocusedIndicatorColor = Color.Unspecified, + ) + val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) + val interactionSource = remember { MutableInteractionSource() } + BasicTextField( + value = searchText, + modifier = modifier + .background(colors.backgroundColor(enabled).value, shape) + .indicatorLine(enabled, false, interactionSource, colors) + .focusRequester(focusRequester) + .defaultMinSize( + minWidth = TextFieldDefaults.MinWidth, + minHeight = TextFieldDefaults.MinHeight + ), + onValueChange = { + searchText = it + onValueChange(it) + }, + cursorBrush = SolidColor(colors.cursorColor(false).value), + visualTransformation = VisualTransformation.None, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + singleLine = true, + textStyle = TextStyle( + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ), + interactionSource = interactionSource, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = searchText, + innerTextField = innerTextField, + placeholder = { + Text(placeholder) + }, + trailingIcon = if (searchText.isNotEmpty()) {{ + IconButton({ searchText = ""; onValueChange("") }) { + Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,) + } + }} else null, + singleLine = true, + enabled = enabled, + interactionSource = interactionSource, + contentPadding = textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = colors + ) + } + ) +} diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 8d483270c2..6aaeed14be 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -233,6 +233,7 @@ SimpleX команда SimpleX логотип Email + Больше Неверный QR код diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index c8a26ff348..90d7fa256b 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -233,6 +233,7 @@ SimpleX Team SimpleX Logo Email + More Invalid QR code From 404b7093b7647324ba831312db1f8e0807adbd7d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 17 Aug 2022 22:50:46 +0100 Subject: [PATCH 24/89] core: update simplexmq (split transaction to fix android crash) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 187a347990..7384a7ae18 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: cfab5a18d3f053b7ccbd61c00d26b3115347f04d + tag: b2d4bfd0a8f7471c2cbd67f195391436bffcdf63 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 88ad06c0a5..b0040f9ffe 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."cfab5a18d3f053b7ccbd61c00d26b3115347f04d" = "1qqg87b4jdkd6drknk95931g3iq28x22swhgyfqmhi8f1m5h8rk0"; + "https://github.com/simplex-chat/simplexmq.git"."b2d4bfd0a8f7471c2cbd67f195391436bffcdf63" = "0j4hn96gc6c6kxrg5fgmcgg1865wc3b54ndr496csd7vz9iyfviv"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 86fe4333dc..5047d02691 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: cfab5a18d3f053b7ccbd61c00d26b3115347f04d + commit: b2d4bfd0a8f7471c2cbd67f195391436bffcdf63 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 5e67654249d4fad61b39c8990f7fa13daf9b67c1 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 18 Aug 2022 11:35:31 +0400 Subject: [PATCH 25/89] core: incognito connections (#926) --- apps/simplex-bot-advanced/Main.hs | 3 +- docs/rfcs/2022-08-10-incognito-connections.md | 51 + package.yaml | 1 + simplex-chat.cabal | 7 + src/Simplex/Chat.hs | 274 +- src/Simplex/Chat/Bot.hs | 4 +- src/Simplex/Chat/Controller.hs | 23 +- src/Simplex/Chat/Help.hs | 4 +- src/Simplex/Chat/Messages.hs | 23 +- .../M20220812_incognito_profiles.hs | 16 + src/Simplex/Chat/Migrations/chat_schema.sql | 5 +- src/Simplex/Chat/ProfileGenerator.hs | 3140 +++++++++++++++++ src/Simplex/Chat/Protocol.hs | 8 +- src/Simplex/Chat/Store.hs | 638 ++-- src/Simplex/Chat/Types.hs | 58 +- src/Simplex/Chat/View.hs | 188 +- tests/ChatTests.hs | 407 ++- tests/MobileTests.hs | 4 +- tests/ProtocolTests.hs | 14 +- 19 files changed, 4408 insertions(+), 460 deletions(-) create mode 100644 docs/rfcs/2022-08-10-incognito-connections.md create mode 100644 src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs create mode 100644 src/Simplex/Chat/ProfileGenerator.hs diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 6b31a62dce..f5acebe96a 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -1,7 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} @@ -40,7 +39,7 @@ mySquaringBot _user cc = do race_ (forever $ void getLine) . forever $ do (_, resp) <- atomically . readTBQueue $ outputQ cc case resp of - CRContactConnected contact -> do + CRContactConnected contact _ -> do contactConnected contact void . sendMsg contact $ "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square" CRNewChatItem (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do diff --git a/docs/rfcs/2022-08-10-incognito-connections.md b/docs/rfcs/2022-08-10-incognito-connections.md new file mode 100644 index 0000000000..e25093cda4 --- /dev/null +++ b/docs/rfcs/2022-08-10-incognito-connections.md @@ -0,0 +1,51 @@ +# Incognito connections + +## Problems + +Allow users to connect with incognito profile using the same user account without exposing main profile - either with randomly generated per connection profile / no profile, or custom profile created by user per connection. The latter option involves designing complex UI for creating profile on connection and seems to detract from UX, so we consider first two options. + +## Proposal + +Add incognito mode determining whether newly created connections are incognito, and a switch on connection pages affecting current connection. + +Add API to turn incognito mode on/off - it is saved as part of ChatController state to allow terminal users to set it. We can save preference on mobile and set it on chat start. We can also persist it to database to carry across terminal sessions, but it seems unnecessary. + +Parameterize `AddContact`, `Connect` and `ConnectSimplex` API - create connection as incognito based on incognito mode and parameter, parameter is given preference. + +### Option 1 - random profile + +Add nullable field `custom_user_profile_id` to `connections` table - `incognitoProfile: Maybe Profile` in `Connection` type; when connection is created as incognito on API call, random profile is created and saved to `profiles` table. + +Incognito profile is created only with a display name, it can be: + +- Some prefix followed by sequence of random character/digits +- Passphrase-like (2-4 random words) +- A name from a dictionary(ies)? +- One of above chosen randomly. + +We could generate other parts of profile (picture?) but it's not necessary for MVP. + +When user initiates connection as incognito, incognito profile is sent as part of XInfo upon receiving CONF from joining user. + +### Option 2 - no profile + +Add `incognito` flag to `connections` table - `incognito: Bool` in `Connection` type. + +Instead of XInfo both in `Connect` API and on receiving CONF when initiating, send a message that doesn't contain profile, e.g. XOk. + +When saving connection profile (`saveConnInfo`) or processing contact request on receiving XOk / other message, contact generates a random profile for the user to distinguish from other connections. He is also able to mark this connection as incognito. + +### Considerations + +- Don't broadcast user profile updates to contacts with whom the user has established incognito connections. +- Add indication on chat info page that connection was established as incognito, show profile name so the user knows how the contact sees him. +- While profile names generated in option 1 may be distinguishable as incognito depending on generator, technically the fact that connection was established as incognito is not explicitly leaked, which is clear with option 2. +- We could offer same random profile generator on creating profiles, which would blend users with such profile as permanent and users who chose to connect with incognito profile to an observer (i.e. the fact that user chooses to be incognito for this specific connection is no longer leaked, just that he chose to be incognito generally). +- There's a use case for custom incognito profile created by user for a given connection in case user wants to hide the fact of incognito connection (leaked by distinguishable pattern in profile name or lack of profile), but it may better be solved by multi-profile. +- Send incognito profile when accepting contact requests in incognito mode? Parameterize API and give option in dialog? + +### Groups + +- If host used user's incognito connection when inviting, save same field marking group as incognito in `groups` table? +- Use incognito profile in XGrpMemInfo +- Allow host to create group in incognito profile - all connections with members are created as incognito? diff --git a/package.yaml b/package.yaml index e0e8dad315..7e5aa0834d 100644 --- a/package.yaml +++ b/package.yaml @@ -31,6 +31,7 @@ dependencies: - network >= 3.1.2.7 && < 3.2 - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* + - random >= 1.1 && < 1.3 - simple-logger == 0.1.* - simplexmq >= 3.0 - socks == 0.6.* diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d78d21f178..33be0c784c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -43,8 +43,10 @@ library Simplex.Chat.Migrations.M20220702_calls Simplex.Chat.Migrations.M20220715_groups_chat_item_id Simplex.Chat.Migrations.M20220811_chat_items_indices + Simplex.Chat.Migrations.M20220812_incognito_profiles Simplex.Chat.Mobile Simplex.Chat.Options + Simplex.Chat.ProfileGenerator Simplex.Chat.Protocol Simplex.Chat.Store Simplex.Chat.Styled @@ -80,6 +82,7 @@ library , network >=3.1.2.7 && <3.2 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplexmq >=3.0 , socks ==0.6.* @@ -120,6 +123,7 @@ executable simplex-bot , network >=3.1.2.7 && <3.2 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplex-chat , simplexmq >=3.0 @@ -161,6 +165,7 @@ executable simplex-bot-advanced , network >=3.1.2.7 && <3.2 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplex-chat , simplexmq >=3.0 @@ -203,6 +208,7 @@ executable simplex-chat , network ==3.1.* , optparse-applicative >=0.15 && <0.17 , process ==1.6.* + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplex-chat , simplexmq >=3.0 @@ -254,6 +260,7 @@ test-suite simplex-chat-test , network ==3.1.* , optparse-applicative >=0.15 && <0.17 , process ==1.6.* + , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplex-chat , simplexmq >=3.0 diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 79e7d33325..eba26284ec 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -31,13 +31,12 @@ import Data.Either (fromRight) import Data.Fixed (div') import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (find, isSuffixOf, sortBy, sortOn) +import Data.List (find, isSuffixOf, sortOn) import Data.List.NonEmpty (NonEmpty, nonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) -import Data.Ord (comparing) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) @@ -51,6 +50,7 @@ import Simplex.Chat.Controller import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Options +import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Types @@ -143,8 +143,9 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty filesFolder <- newTVarIO Nothing + incognitoMode <- newTVarIO False chatStoreChanged <- newTVarIO False - pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder} + pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder} where resolveServers :: InitialAgentServers -> IO InitialAgentServers resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of @@ -233,6 +234,10 @@ processChatCommand = \case ff <- asks filesFolder atomically . writeTVar ff $ Just filesFolder' pure CRCmdOk + SetIncognito onOff -> do + incognito <- asks incognitoMode + atomically . writeTVar incognito $ onOff + pure CRCmdOk APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk @@ -254,16 +259,14 @@ processChatCommand = \case pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci where setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer ct = case file_ of - Nothing -> pure Nothing - Just file -> do - (fileSize, chSize) <- checkSndFile file - (agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) - let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} - fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} - pure $ Just (fileInvitation, ciFile) + setupSndFileTransfer ct = forM file_ $ \file -> do + (fileSize, chSize) <- checkSndFile file + (agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) + let fileName = takeFileName file + fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} + fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize + let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} + pure (fileInvitation, ciFile) prepareMsg :: Maybe FileInvitation -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) prepareMsg fileInvitation_ = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) @@ -291,15 +294,13 @@ processChatCommand = \case pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci where setupSndFileTransfer :: GroupInfo -> m (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer gInfo = case file_ of - Nothing -> pure Nothing - Just file -> do - (fileSize, chSize) <- checkSndFile file - let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing} - fileId <- withStore' $ \db -> createSndGroupFileTransfer db userId gInfo file fileInvitation chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} - pure $ Just (fileInvitation, ciFile) + setupSndFileTransfer gInfo = forM file_ $ \file -> do + (fileSize, chSize) <- checkSndFile file + let fileName = takeFileName file + fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing} + fileId <- withStore' $ \db -> createSndGroupFileTransfer db userId gInfo file fileInvitation chSize + let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} + pure (fileInvitation, ciFile) prepareMsg :: Maybe FileInvitation -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup)) prepareMsg fileInvitation_ membership = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) @@ -597,11 +598,17 @@ processChatCommand = \case APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk APIGetNetworkConfig -> CRNetworkConfig <$> withUser' (\_ -> withAgent getNetworkConfig) APIContactInfo contactId -> withUser $ \User {userId} -> do - ct <- withStore $ \db -> getContact db userId contactId - CRContactInfo ct <$> withAgent (`getConnectionServers` contactConnId ct) - APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId - CRGroupMemberInfo g m <$> mapM (withAgent . flip getConnectionServers) (memberConnId m) + -- [incognito] print user's incognito profile for this contact + ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db userId contactId + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + connectionStats <- withAgent (`getConnectionServers` contactConnId ct) + pure $ CRContactInfo ct connectionStats incognitoProfile + APIGroupMemberInfo gId gMemberId -> withUser $ \user@User {userId} -> do + -- [incognito] print group member main profile + (g, m@GroupMember {memberContactProfileId}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId + mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) + pure $ CRGroupMemberInfo g m connectionStats mainProfile ContactInfo cName -> withUser $ \User {userId} -> do contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APIContactInfo contactId @@ -611,20 +618,29 @@ processChatCommand = \case ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome AddContact -> withUser $ \User {userId} -> withChatLock . procCmd $ do + -- [incognito] generate profile for connection + incognito <- readTVarIO =<< asks incognitoMode + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing (connId, cReq) <- withAgent (`createConnection` SCMInvitation) - conn <- withStore' $ \db -> createDirectConnection db userId connId ConnNew + conn <- withStore' $ \db -> createDirectConnection db userId connId ConnNew incognitoProfile toView $ CRNewContactConnection conn pure $ CRInvitation cReq Connect (Just (ACR SCMInvitation cReq)) -> withUser $ \User {userId, profile} -> withChatLock . procCmd $ do - connId <- withAgent $ \a -> joinConnection a cReq . directMessage $ XInfo profile - conn <- withStore' $ \db -> createDirectConnection db userId connId ConnJoined + -- [incognito] generate profile to send + incognito <- readTVarIO =<< asks incognitoMode + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile + connId <- withAgent $ \a -> joinConnection a cReq . directMessage $ XInfo profileToSend + conn <- withStore' $ \db -> createDirectConnection db userId connId ConnJoined incognitoProfile toView $ CRNewContactConnection conn pure CRSentConfirmation Connect (Just (ACR SCMContact cReq)) -> withUser $ \User {userId, profile} -> - connectViaContact userId cReq profile + -- [incognito] generate profile to send + connectViaContact userId cReq $ fromLocalProfile profile Connect Nothing -> throwChatError CEInvalidConnReq ConnectSimplex -> withUser $ \User {userId, profile} -> - connectViaContact userId adminContactReq profile + -- [incognito] generate profile to send + connectViaContact userId adminContactReq $ fromLocalProfile profile DeleteContact cName -> withUser $ \User {userId} -> do contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId) @@ -686,29 +702,37 @@ processChatCommand = \case processChatCommand $ APIUpdateChatItem chatRef editedItemId mc NewGroup gProfile -> withUser $ \user -> do gVar <- asks idsDrg - CRGroupCreated <$> withStore (\db -> createNewGroup db gVar user gProfile) + -- [incognito] create membership with incognito profile + incognito <- readTVarIO =<< asks incognitoMode + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile incognitoProfile) + pure $ CRGroupCreated groupInfo incognitoProfile APIAddMember groupId contactId memRole -> withUser $ \user@User {userId} -> withChatLock $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db userId contactId + -- [incognito] forbid to invite contact to whom user is connected as incognito if user's membership is not incognito let Group gInfo@GroupInfo {localDisplayName, groupProfile, membership} members = group GroupMember {memberRole = userRole, memberId = userMemberId} = membership Contact {localDisplayName = cName} = contact + when (contactConnIncognito contact && not (memberIncognito membership)) $ throwChatError CEGroupNotIncognitoCantInvite when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo) unless (memberActive membership) $ throwChatError CEGroupMemberNotActive let sendInvitation member@GroupMember {groupMemberId, memberId} cReq = do - let groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile + -- [incognito] if membership is incognito, send its incognito profile in GroupInvitation + let incognitoProfile = if memberIncognito membership then Just (fromLocalProfile $ memberProfile membership) else Nothing + groupInv = GroupInvitation (MemberIdRole userMemberId userRole) incognitoProfile (MemberIdRole memberId memRole) cReq groupProfile msg <- sendDirectContactMessage contact $ XGrpInv groupInv - let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole + let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just $ memberIncognito membership}) memRole ci <- saveSndChatItem user (CDDirectSnd contact) msg content Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat contact) ci setActive $ ActiveG localDisplayName - pure $ CRSentGroupInvitation gInfo contact member + pure $ CRSentGroupInvitation gInfo contact member incognitoProfile case contactMember contact members of Nothing -> do gVar <- asks idsDrg (agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation) - member <- withStore $ \db -> createContactMember db gVar user groupId contact memRole agentConnId cReq + member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation member cReq Just member@GroupMember {groupMemberId, memberStatus} | memberStatus == GSMemInvited -> @@ -719,13 +743,30 @@ processChatCommand = \case APIJoinGroup groupId -> withUser $ \user@User {userId} -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId withChatLock . procCmd $ do - agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage . XGrpAcpt $ memberId (membership :: GroupMember) + -- [incognito] if (incognito mode is enabled OR direct connection with host is incognito) [AND membership is not incognito] update membership to use incognito profile + incognito <- readTVarIO =<< asks incognitoMode + hostConnIncognito <- case (incognito, memberContactId fromMember) of + (True, _) -> pure True -- we don't need to check whether connection with host is incognito if incognito mode is on + (_, Just hostContactId) -> do + hostContact <- withStore $ \db -> getContact db userId hostContactId + pure $ contactConnIncognito hostContact + _ -> pure False + g'@GroupInfo {membership = membership'} <- + if (incognito || hostConnIncognito) && not (memberIncognito membership) + then do + incognitoProfile <- liftIO generateRandomProfile + membership' <- withStore $ \db -> createMemberIncognitoProfile db userId membership (Just incognitoProfile) + pure g {membership = membership'} + else pure g + -- [incognito] if membership is incognito, send its incognito profile in XGrpAcpt + let incognitoProfile = if memberIncognito membership' then Just (fromLocalProfile $ memberProfile membership') else Nothing + agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId updateGroupMemberStatus db userId fromMember GSMemAccepted - updateGroupMemberStatus db userId membership GSMemAccepted + updateGroupMemberStatus db userId membership' GSMemAccepted updateCIGroupInvitationStatus user - pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}} + pure $ CRUserAcceptedGroupSent g' {membership = membership' {memberStatus = GSMemAccepted}} where updateCIGroupInvitationStatus user@User {userId} = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db user groupId @@ -750,7 +791,7 @@ processChatCommand = \case withStore' $ \db -> deleteGroupMember db user m _ -> do msg <- sendGroupMessage gInfo members $ XGrpMemDel mId - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId memberProfile) Nothing Nothing + ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) Nothing Nothing toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci deleteMemberConnection m withStore' $ \db -> updateGroupMemberStatus db userId m GSMemRemoved @@ -861,12 +902,12 @@ processChatCommand = \case pure $ CRRcvFileCancelled ftr FileStatus fileId -> CRFileTransferStatus <$> withUser (\user -> withStore $ \db -> getFileTransferProgress db user fileId) - ShowProfile -> withUser $ \User {profile} -> pure $ CRUserProfile profile + ShowProfile -> withUser $ \User {profile} -> pure $ CRUserProfile (fromLocalProfile profile) UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do - let p = (profile :: Profile) {displayName = displayName, fullName = fullName} + let p = (fromLocalProfile profile :: Profile) {displayName = displayName, fullName = fullName} updateProfile user p UpdateProfileImage image -> withUser $ \user@User {profile} -> do - let p = (profile :: Profile) {image} + let p = (fromLocalProfile profile :: Profile) {image} updateProfile user p QuitChat -> liftIO exitSuccess ShowVersion -> pure $ CRVersionInfo versionNumber @@ -910,10 +951,18 @@ processChatCommand = \case (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) xContactId <- maybe randomXContactId pure xContactId_ - connId <- withAgent $ \a -> joinConnection a cReq $ directMessage (XContact profile $ Just xContactId) - conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId + -- [incognito] generate profile to send + -- if user makes a contact request using main profile, then turns on incognito mode and repeats the request, + -- an incognito profile will be sent even though the address holder will have user's main profile received as well; + -- we ignore this edge case as we already allow profile updates on repeat contact requests; + -- alternatively we can re-send the main profile even if incognito mode is enabled + incognito <- readTVarIO =<< asks incognitoMode + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = fromMaybe profile incognitoProfile + connId <- withAgent $ \a -> joinConnection a cReq $ directMessage (XContact profileToSend $ Just xContactId) + conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile toView $ CRNewContactConnection conn - pure CRSentInvitation + pure $ CRSentInvitation incognitoProfile contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -924,17 +973,20 @@ processChatCommand = \case unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f (,) <$> getFileSize fsFilePath <*> asks (fileChunkSize . config) updateProfile :: User -> Profile -> m ChatResponse - updateProfile user@User {profile = p} p'@Profile {displayName} - | p' == p = pure CRUserProfileNoChange + updateProfile user@User {profile = p@LocalProfile {profileId}} p'@Profile {displayName} + | p' == fromLocalProfile p = pure CRUserProfileNoChange | otherwise = do withStore $ \db -> updateUserProfile db user p' - let user' = (user :: User) {localDisplayName = displayName, profile = p'} + let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p'} asks currentUser >>= atomically . (`writeTVar` Just user') - contacts <- filter isReady <$> withStore' (`getUserContacts` user) + -- [incognito] filter out contacts with whom user has incognito connections + contacts <- + filter (\ct -> isReady ct && not (contactConnIncognito ct)) + <$> withStore' (`getUserContacts` user) withChatLock . procCmd $ do forM_ contacts $ \ct -> void (sendDirectContactMessage ct $ XInfo p') `catchError` (toView . CRChatError) - pure $ CRUserProfileUpdated p p' + pure $ CRUserProfileUpdated (fromLocalProfile p) p' isReady :: Contact -> Bool isReady ct = let s = connStatus $ activeConn (ct :: Contact) @@ -1117,8 +1169,12 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> m Contact acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} = do - connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profile - withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId + -- [incognito] generate profile to send, create connection with incognito profile + incognito <- readTVarIO =<< asks incognitoMode + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile + connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profileToSend + withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId incognitoProfile agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () agentSubscriber = do @@ -1192,7 +1248,7 @@ subscribeUserConnections agentBatchSubscribe user = do groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m () groupSubsToView rs gs ms ce = do mapM_ groupSub $ - sortBy (comparing $ \(Group GroupInfo {localDisplayName = g} _) -> g) gs + sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs toView . CRMemberSubSummary $ map (uncurry MemberSubStatus) mRs where mRs = resultsFor rs ms @@ -1293,11 +1349,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> Nothing processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink} = \case + processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink, customUserProfileId} = \case Nothing -> case agentMsg of CONF confId _ connInfo -> do + -- [incognito] send saved profile + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile saveConnInfo conn connInfo - allowAgentConnection conn confId $ XInfo profile + allowAgentConnection conn confId $ XInfo profileToSend INFO connInfo -> saveConnInfo conn connInfo MSG meta _msgFlags msgBody -> do @@ -1358,7 +1417,9 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage CON -> withStore' (\db -> getViaGroupMember db user ct) >>= \case Nothing -> do - toView $ CRContactConnected ct + -- [incognito] print incognito profile used for this contact + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + toView $ CRContactConnected ct incognitoProfile setActive $ ActiveC c showToast (c <> "> ") "connected" forM_ viaUserContactLink $ \userContactLinkId -> do @@ -1386,25 +1447,26 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage -- TODO print errors MERR msgId err -> do chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId - case chatItemId_ of - Nothing -> pure () - Just chatItemId -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId chatItemId (agentErrToItemStatus err) - toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) + forM_ chatItemId_ $ \chatItemId -> do + chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId chatItemId (agentErrToItemStatus err) + toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) ERR err -> toView . CRChatError $ ChatErrorAgent err -- TODO add debugging output _ -> pure () processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m () - processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership} m = case agentMsg of + processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership} m@GroupMember {memberContactProfileId} = case agentMsg of CONF confId _ connInfo -> do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo case memberCategory m of GCInviteeMember -> case chatMsgEvent of - XGrpAcpt memId + XGrpAcpt memId incognitoProfile | sameMemberId memId m -> do - withStore' $ \db -> updateGroupMemberStatus db userId m GSMemAccepted + -- [incognito] update member profile to incognito profile + withStore $ \db -> do + liftIO $ updateGroupMemberStatus db userId m GSMemAccepted + void $ createMemberIncognitoProfile db userId m incognitoProfile allowAgentConnection conn confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" _ -> messageError "CONF from invited member must have x.grp.acpt" @@ -1413,7 +1475,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage XGrpMemInfo memId _memProfile | sameMemberId memId m -> do -- TODO update member profile - allowAgentConnection conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) profile + -- [incognito] send membership incognito profile + allowAgentConnection conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" _ -> messageError "CONF from member must have x.grp.mem.info" INFO connInfo -> do @@ -1436,13 +1499,17 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage sendPendingGroupMessages m conn case memberCategory m of GCHostMember -> do - memberConnectedChatItem gInfo m - toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} + -- [incognito] chat item & event with indication that host connected incognito + mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + memberConnectedChatItem gInfo m mainProfile + toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} (memberIncognito membership) setActive $ ActiveG gName showToast ("#" <> gName) "you are connected to group" GCInviteeMember -> do - memberConnectedChatItem gInfo m - toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} + -- [incognito] chat item & event with indication that invitee connected incognito + mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + memberConnectedChatItem gInfo m mainProfile + toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} mainProfile setActive $ ActiveG gName showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected" intros <- withStore' $ \db -> createIntroductions db members m @@ -1629,10 +1696,12 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage cancelRcvFileTransfer user ft throwChatError $ CEFileRcvChunk err - memberConnectedChatItem :: GroupInfo -> GroupMember -> m () - memberConnectedChatItem gInfo m = do + memberConnectedChatItem :: GroupInfo -> GroupMember -> Maybe Profile -> m () + memberConnectedChatItem gInfo m mainProfile_ = do createdAt <- liftIO getCurrentTime - let content = CIRcvGroupEvent RGEMemberConnected + let content = CIRcvGroupEvent $ case mainProfile_ of + Just mainProfile -> RGEMemberConnected $ Just mainProfile + _ -> RGEMemberConnected Nothing cd = CDGroupRcv gInfo m -- first ts should be broker ts but we don't have it for CON ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content createdAt createdAt @@ -1641,7 +1710,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage notifyMemberConnected :: GroupInfo -> GroupMember -> m () notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do - memberConnectedChatItem gInfo m + memberConnectedChatItem gInfo m Nothing toView $ CRConnectedToGroupMember gInfo m let g = groupName' gInfo setActive $ ActiveG g @@ -1679,13 +1748,12 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage setActive $ ActiveC c processFileInvitation :: Maybe FileInvitation -> (FileInvitation -> Integer -> m RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) - processFileInvitation fileInvitation_ createRcvFileTransferF = case fileInvitation_ of - Nothing -> pure Nothing - Just fileInvitation@FileInvitation {fileName, fileSize} -> do + processFileInvitation fileInvitation_ createRcvFileTransferF = + forM fileInvitation_ $ \fileInvitation@FileInvitation {fileName, fileSize} -> do chSize <- asks $ fileChunkSize . config RcvFileTransfer {fileId} <- createRcvFileTransferF fileInvitation chSize let ciFile = CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} - pure $ Just ciFile + pure ciFile messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m () messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta = do @@ -1830,16 +1898,19 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () - processGroupInvitation ct@Contact {localDisplayName = c} inv@(GroupInvitation (MemberIdRole fromMemId fromRole) (MemberIdRole memId memRole) _ _) msg msgMeta = do + processGroupInvitation ct@Contact {localDisplayName = c} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), fromMemberProfile, invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId - gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv - let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole + -- [incognito] if received group invitation has host's incognito profile, create membership with new incognito profile; incognito mode is checked when joining group + let invitedIncognito = isJust fromMemberProfile + incognitoProfile <- if invitedIncognito then Just <$> liftIO generateRandomProfile else pure Nothing + gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv incognitoProfile + let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just invitedIncognito}) memRole ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - toView $ CRReceivedGroupInvitation gInfo ct memRole + toView $ CRReceivedGroupInvitation gInfo ct memRole fromMemberProfile showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group" checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m () @@ -1857,23 +1928,27 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView $ CRNewChatItem $ AChatItem (chatTypeI @c) SMDRcv (toChatInfo cd) ci xInfo :: Contact -> Profile -> m () - xInfo c@Contact {profile = p} p' = unless (p == p') $ do + xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do c' <- withStore $ \db -> updateContactProfile db userId c p' toView $ CRContactUpdated c c' xInfoProbe :: Contact -> Probe -> m () - xInfoProbe c2 probe = do - r <- withStore' $ \db -> matchReceivedProbe db userId c2 probe - forM_ r $ \c1 -> probeMatch c1 c2 probe + xInfoProbe c2 probe = + -- [incognito] unless connected incognito + unless (contactConnIncognito c2) $ do + r <- withStore' $ \db -> matchReceivedProbe db userId c2 probe + forM_ r $ \c1 -> probeMatch c1 c2 probe xInfoProbeCheck :: Contact -> ProbeHash -> m () - xInfoProbeCheck c1 probeHash = do - r <- withStore' $ \db -> matchReceivedProbeHash db userId c1 probeHash - forM_ r . uncurry $ probeMatch c1 + xInfoProbeCheck c1 probeHash = + -- [incognito] unless connected incognito + unless (contactConnIncognito c1) $ do + r <- withStore' $ \db -> matchReceivedProbeHash db userId c1 probeHash + forM_ r . uncurry $ probeMatch c1 probeMatch :: Contact -> Contact -> Probe -> m () probeMatch c1@Contact {profile = p1} c2@Contact {profile = p2} probe = - when (p1 == p2) $ do + when (fromLocalProfile p1 == fromLocalProfile p2) $ do void . sendDirectContactMessage c1 $ XInfoProbeOk probe mergeContacts c1 c2 @@ -2009,7 +2084,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView $ CRJoinedGroupMemberConnecting gInfo m newMember xGrpMemIntro :: Connection -> GroupInfo -> GroupMember -> MemberInfo -> m () - xGrpMemIntro conn gInfo@GroupInfo {groupId} m memInfo@(MemberInfo memId _ _) = do + xGrpMemIntro conn gInfo@GroupInfo {groupId, membership} m memInfo@(MemberInfo memId _ _) = do case memberCategory m of GCHostMember -> do members <- withStore' $ \db -> getGroupMembers db user gInfo @@ -2018,7 +2093,9 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage else do (groupConnId, groupConnReq) <- withAgent (`createConnection` SCMInvitation) (directConnId, directConnReq) <- withAgent (`createConnection` SCMInvitation) - newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId + -- [incognito] direct connection with member has to be established using same incognito profile + customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing + newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId customUserProfileId let msg = XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq} void $ sendDirectMessage conn msg (GroupId groupId) withStore' $ \db -> updateGroupMemberStatus db userId newMember GSMemIntroInvited @@ -2047,10 +2124,12 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage Nothing -> withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced Just m' -> pure m' withStore' $ \db -> saveMemberInvitation db toMember introInv - let msg = XGrpMemInfo (memberId (membership :: GroupMember)) profile + -- [incognito] send membership incognito profile, create direct connection as incognito + let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) groupConnId <- withAgent $ \a -> joinConnection a groupConnReq $ directMessage msg directConnId <- withAgent $ \a -> joinConnection a directConnReq $ directMessage msg - withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId + customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing + withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId customUserProfileId xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> MsgMeta -> m () xGrpMemDel gInfo@GroupInfo {membership} m memId msg msgMeta = do @@ -2071,7 +2150,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage else do deleteMemberConnection member withStore' $ \db -> updateGroupMemberStatus db userId member GSMemRemoved - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent $ RGEMemberDeleted groupMemberId memberProfile) Nothing + ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)) Nothing groupMsgToView gInfo m ci msgMeta toView $ CRDeletedMember gInfo m member {memberStatus = GSMemRemoved} @@ -2388,7 +2467,7 @@ getCreateActiveUser st = do withTransaction st (`setActiveUser` userId user) pure user userStr :: User -> String - userStr User {localDisplayName, profile = Profile {fullName}} = + userStr User {localDisplayName, profile = LocalProfile {fullName}} = T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" getContactName :: IO ContactName getContactName = do @@ -2556,6 +2635,7 @@ chatCommandP = "/profile_image" $> UpdateProfileImage Nothing, ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> userNames), ("/profile" <|> "/p") $> ShowProfile, + "/incognito " *> (SetIncognito <$> onOffP), ("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/version" <|> "/v") $> ShowVersion ] diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index ef055143a6..0fb2812f5d 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -24,7 +24,7 @@ chatBotRepl welcome answer _user cc = do race_ (forever $ void getLine) . forever $ do (_, resp) <- atomically . readTBQueue $ outputQ cc case resp of - CRContactConnected contact -> do + CRContactConnected contact _ -> do contactConnected contact void $ sendMsg contact welcome CRNewChatItem (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do @@ -40,7 +40,7 @@ initializeBotAddress cc = do sendChatCmd cc "/show_address" >>= \case CRUserContactLink uri _ _ -> showBotAddress uri CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do - putStrLn $ "No bot address, creating..." + putStrLn "No bot address, creating..." sendChatCmd cc "/address" >>= \case CRUserContactLinkCreated uri -> showBotAddress uri _ -> putStrLn "can't create bot address" >> exitFailure diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index a606cf2f11..173bd90382 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -88,7 +88,8 @@ data ChatController = ChatController rcvFiles :: TVar (Map Int64 Handle), currentCalls :: TMap ContactId Call, config :: ChatConfig, - filesFolder :: TVar (Maybe FilePath) -- path to files folder for mobile apps + filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, + incognitoMode :: TVar Bool } data HelpSection = HSMain | HSFiles | HSGroups | HSMyAddress | HSMarkdown | HSMessages | HSSettings @@ -107,6 +108,7 @@ data ChatCommand | APISuspendChat {suspendTimeout :: Int} | ResubscribeAllConnections | SetFilesFolder FilePath + | SetIncognito Bool | APIExportArchive ArchiveConfig | APIImportArchive ArchiveConfig | APIDeleteStorage @@ -210,8 +212,8 @@ data ChatResponse | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} | CRUserSMPServers {smpServers :: [SMPServer]} | CRNetworkConfig {networkConfig :: NetworkConfig} - | CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats} - | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} + | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats, mainProfile :: Maybe Profile} | CRNewChatItem {chatItem :: AChatItem} | CRChatItemStatusUpdated {chatItem :: AChatItem} | CRChatItemUpdated {chatItem :: AChatItem} @@ -223,7 +225,7 @@ data ChatResponse | CRCmdOk | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} - | CRGroupCreated {groupInfo :: GroupInfo} + | CRGroupCreated {groupInfo :: GroupInfo, customUserProfile :: Maybe Profile} | CRGroupMembers {group :: Group} | CRContactsList {contacts :: [Contact]} | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} @@ -232,14 +234,14 @@ data ChatResponse | CRUserAcceptedGroupSent {groupInfo :: GroupInfo} | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} | CRGroupsList {groups :: [GroupInfo]} - | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} + | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember, sentCustomProfile :: Maybe Profile} | CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus | CRUserProfile {profile :: Profile} | CRUserProfileNoChange | CRVersionInfo {version :: String} | CRInvitation {connReqInvitation :: ConnReqInvitation} | CRSentConfirmation - | CRSentInvitation + | CRSentInvitation {customUserProfile :: Maybe Profile} | CRContactUpdated {fromContact :: Contact, toContact :: Contact} | CRContactsMerged {intoContact :: Contact, mergedContact :: Contact} | CRContactDeleted {contact :: Contact} @@ -265,7 +267,7 @@ data ChatResponse | CRSndGroupFileCancelled {chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} | CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile} | CRContactConnecting {contact :: Contact} - | CRContactConnected {contact :: Contact} + | CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile} | CRContactAnotherClient {contact :: Contact} | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} @@ -274,9 +276,9 @@ data ChatResponse | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRGroupInvitation {groupInfo :: GroupInfo} - | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} - | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember} - | CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember} + | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole, receivedCustomProfile :: Maybe Profile} + | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember, usedCustomProfile :: Bool} + | CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember, mainProfile :: Maybe Profile} | CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} | CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember} | CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} @@ -383,6 +385,7 @@ data ChatErrorType | CEContactNotReady {contact :: Contact} | CEContactGroups {contact :: Contact, groupNames :: [GroupName]} | CEGroupUserRole + | CEGroupNotIncognitoCantInvite | CEGroupContactRole {contactName :: ContactName} | CEGroupDuplicateMember {contactName :: ContactName} | CEGroupDuplicateMemberId diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index f35cd39ec7..7872284ae9 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -18,7 +18,7 @@ import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Markdown import Simplex.Chat.Styled -import Simplex.Chat.Types (Profile (..), User (..)) +import Simplex.Chat.Types (User (..), LocalProfile (..)) import System.Console.ANSI.Types highlight :: Text -> Markdown @@ -55,7 +55,7 @@ chatWelcome user = "Type " <> highlight "/help" <> " for usage info, " <> highlight "/welcome" <> " to show this message" ] where - User {profile = Profile {displayName, fullName}} = user + User {profile = LocalProfile {displayName, fullName}} = user userName = if T.null fullName then displayName else fullName chatHelpInfo :: [StyledString] diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 5be8deb6c5..02fa60d937 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -501,22 +501,24 @@ ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayN rcvGroupEventToText :: RcvGroupEvent -> Text rcvGroupEventToText = \case - RGEMemberAdded _ p -> "added " <> memberProfileToText p - RGEMemberConnected -> "connected" + RGEMemberAdded _ p -> "added " <> profileToText p + RGEMemberConnected contactMainProfile -> case contactMainProfile of + Just p -> profileToText p <> " connected incognito" + Nothing -> "connected" RGEMemberLeft -> "left" - RGEMemberDeleted _ p -> "removed " <> memberProfileToText p + RGEMemberDeleted _ p -> "removed " <> profileToText p RGEUserDeleted -> "removed you" RGEGroupDeleted -> "deleted group" RGEGroupUpdated _ -> "group profile updated" sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case - SGEMemberDeleted _ p -> "removed " <> memberProfileToText p + SGEMemberDeleted _ p -> "removed " <> profileToText p SGEUserLeft -> "left" SGEGroupUpdated _ -> "group profile updated" -memberProfileToText :: Profile -> Text -memberProfileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName +profileToText :: Profile -> Text +profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName -- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API data CIContent (d :: MsgDirection) where @@ -536,7 +538,7 @@ deriving instance Show (CIContent d) data RcvGroupEvent = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting - | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember + | RGEMemberConnected {contactMainProfile :: Maybe Profile} -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember | RGEMemberLeft -- CRLeftMember | RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember | RGEUserDeleted -- CRDeletedMemberUser @@ -569,13 +571,14 @@ data CIGroupInvitation = CIGroupInvitation groupMemberId :: GroupMemberId, localDisplayName :: GroupName, groupProfile :: GroupProfile, - status :: CIGroupInvitationStatus + status :: CIGroupInvitationStatus, + invitedIncognito :: Maybe Bool } deriving (Eq, Show, Generic, FromJSON) instance ToJSON CIGroupInvitation where - toJSON = J.genericToJSON J.defaultOptions - toEncoding = J.genericToEncoding J.defaultOptions + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} data CIGroupInvitationStatus = CIGISPending diff --git a/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs b/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs new file mode 100644 index 0000000000..2ece1be90d --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220812_incognito_profiles where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220812_incognito_profiles :: Query +m20220812_incognito_profiles = + [sql| +ALTER TABLE connections ADD COLUMN custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- only set for direct connections + +ALTER TABLE group_members ADD COLUMN member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- member profile id if incognito profile was saved for member (used for hosts and invitees in incognito groups) + +ALTER TABLE contact_profiles ADD COLUMN incognito INTEGER; -- 1 for incognito +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 8ef4889572..ce4c6553fd 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -13,7 +13,8 @@ CREATE TABLE contact_profiles( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), image TEXT, - user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE + user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE, + incognito INTEGER ); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, @@ -145,6 +146,7 @@ CREATE TABLE group_members( contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), + member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -236,6 +238,7 @@ CREATE TABLE connections( xcontact_id BLOB, via_user_contact_link INTEGER DEFAULT NULL REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL, + custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs new file mode 100644 index 0000000000..c42cd6ab66 --- /dev/null +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -0,0 +1,3140 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.ProfileGenerator where + +import Data.Text (Text) +import Simplex.Chat.Types (Profile (..)) +import System.Random (randomRIO) + +generateRandomProfile :: IO Profile +generateRandomProfile = do + adjective <- pick adjectives + noun <- pickNoun adjective 2 + pure $ Profile {displayName = adjective <> noun, fullName = "", image = Nothing} + where + pick :: [a] -> IO a + pick xs = (xs !!) <$> randomRIO (0, length xs - 1) + pickNoun :: Text -> Int -> IO Text + pickNoun adjective n + | n == 0 = pick nouns + | otherwise = do + noun <- pick nouns + if noun == adjective + then pickNoun adjective (n - 1) + else pure noun + +adjectives :: [Text] +adjectives = + [ "Abatic", + "Abducent", + "Abecedarian", + "Aberrant", + "Abeyant", + "Abject", + "Ablative", + "Ablaze", + "Able", + "Abloom", + "Ablutionary", + "Abolishable", + "Abolitionary", + "Aboriginal", + "Aboulic", + "Abounding", + "About", + "Aboveboard", + "Aboveground", + "Abranchial", + "Abranchiate", + "Abranchious", + "Abrasive", + "Abrupt", + "Abscessed", + "Absent", + "Absolute", + "Absolved", + "Absorbable", + "Absorbed", + "Absorbing", + "Absorptive", + "Abstinent", + "Abstracted", + "Abstractionist", + "Abundant", + "Abysmal", + "Abyssal", + "Academic", + "Acceptable", + "Accessible", + "Acclaimed", + "Accommodating", + "Accomplished", + "Accordant", + "Accurate", + "Acknowledged", + "Acrobatic", + "Active", + "Actual", + "Adamant", + "Adaptable", + "Adept", + "Adequate", + "Adhesive", + "Adhoc", + "Adjusted", + "Admirable", + "Admired", + "Admissible", + "Adorable", + "Adored", + "Adroit", + "Advantaged", + "Adventuresome", + "Adventurous", + "Aesthetic", + "Aesthetical", + "Affable", + "Affecting", + "Affectionate", + "Affiliated", + "Affluent", + "Aged", + "Ageless", + "Agile", + "Agitated", + "Agonizing", + "Agreeable", + "Ahead", + "Aholic", + "Alarmed", + "Alarming", + "Alert", + "Alienated", + "Alike", + "Alive", + "Alleged", + "Allied", + "Alright", + "Amative", + "Amatory", + "Ambiguous", + "Ambitious", + "Amelioratory", + "Amenable", + "Amicable", + "Ample", + "Amused", + "Amusing", + "Anchored", + "Ancient", + "Angelic", + "Animated", + "Annual", + "Antsy", + "Appealing", + "Appetent", + "Apposite", + "Apprehensible", + "Apprehensive", + "Approachable", + "Apropos", + "Apt", + "Aquatic", + "Arctic", + "Arid", + "Aromatic", + "Arousing", + "Arrogant", + "Articulate", + "Artistic", + "Aspirant", + "Assertive", + "Assiduous", + "Assistant", + "Associate", + "Associative", + "Assured", + "Assuring", + "Astonishing", + "Astounding", + "Astute", + "Athletic", + "Attached", + "Attainable", + "Attendant", + "Attentive", + "Attractive", + "August", + "Auspicious", + "Authentic", + "Automatic", + "Autonomous", + "Available", + "Awaited", + "Awake", + "Aware", + "Awash", + "Awesome", + "Babyish", + "Back", + "Baggy", + "Balanced", + "Bare", + "Barren", + "Basic", + "Beaming", + "Beatific", + "Beauteous", + "Beautified", + "Beautiful", + "Becoming", + "Beefy", + "Belated", + "Believable", + "Beloved", + "Benedictory", + "Benefic", + "Beneficent", + "Beneficial", + "Beneficiary", + "Benevolent", + "Benign", + "Benignant", + "Best", + "Better", + "Bewitched", + "Big", + "Biggest", + "Bijou", + "Bitter", + "Blameless", + "Bland", + "Blank", + "Blaring", + "Blazing", + "Blessed", + "Blissful", + "Blithe", + "Blond", + "Blooming", + "Blue", + "Blushing", + "Bodacious", + "Bogus", + "Boiling", + "Boisterous", + "Bold", + "Bonafide", + "Bonny", + "Bony", + "Bonzer", + "Boring", + "Boss", + "Bossy", + "Both", + "Bouncy", + "Bound", + "Bounteous", + "Bountiful", + "Bowed", + "Brainy", + "Brave", + "Brawny", + "Breakable", + "Breezy", + "Brief", + "Bright", + "Brill", + "Brilliant", + "Brimming", + "Brisk", + "Broadminded", + "Broken", + "Bronze", + "Bruised", + "Bubbly", + "Budding", + "Buff", + "Bulky", + "Bullish", + "Bumpy", + "Buoyant", + "Burdensome", + "Burly", + "Bustling", + "Busy", + "Buttery", + "Buzzing", + "Calm", + "Calmative", + "Calming", + "Candescent", + "Canny", + "Canty", + "Capable", + "Capital", + "Captivating", + "Carefree", + "Careful", + "Caring", + "Casual", + "Causative", + "Celebrated", + "Celestial", + "Centered", + "Central", + "Cerebral", + "Certain", + "Champion", + "Changeable", + "Changeless", + "Charismatic", + "Charitable", + "Charming", + "Cheerful", + "Cherished", + "Cherry", + "Chic", + "Childlike", + "Chipper", + "Chirpy", + "Chosen", + "Chummy", + "Civic", + "Civil", + "Civilized", + "Clairvoyant", + "Classic", + "Classical", + "Classy", + "Clean", + "Clear", + "Clearheaded", + "Clement", + "Clever", + "Close", + "Clubby", + "Coadjutant", + "Coequal", + "Cogent", + "Cognizant", + "Coherent", + "Collected", + "Comfortable", + "Comforting", + "Comic", + "Comical", + "Commanding", + "Commendable", + "Commendatory", + "Commending", + "Commiserative", + "Committed", + "Commodious", + "Commonsensical", + "Communicative", + "Commutual", + "Companionable", + "Compassionate", + "Compatible", + "Compelling", + "Competent", + "Complete", + "Completed", + "Complimentary", + "Composed", + "Comprehensive", + "Concentrated", + "Concise", + "Conclusive", + "Concordant", + "Concrete", + "Condolatory", + "Confederate", + "Conferrable", + "Confident", + "Congenial", + "Consentient", + "Consequential", + "Considerable", + "Considerate", + "Consistent", + "Consonant", + "Conspicuous", + "Constant", + "Constitutional", + "Constructive", + "Contemplative", + "Contemporary", + "Content", + "Contributive", + "Convenient", + "Conversant", + "Convictive", + "Convincing", + "Convivial", + "Cool", + "Cooperative", + "Coordinated", + "Copacetic", + "Copious", + "Cordial", + "Correct", + "Coruscant", + "Cosmic", + "Courageous", + "Courteous", + "Courtly", + "Cozy", + "Crackerjack", + "Creative", + "Credible", + "Creditable", + "Crisp", + "Crucial", + "Crystal", + "Cuddly", + "Cultivated", + "Cultured", + "Cunning", + "Curious", + "Current", + "Cushy", + "Cute", + "Dainty", + "Dandy", + "Dapper", + "Daring", + "Dark", + "Darling", + "Dashing", + "Dauntless", + "Dazzling", + "Dear", + "Dearest", + "Debonair", + "Decent", + "Deciding", + "Decimal", + "Decisive", + "Decorous", + "Dedicated", + "Deep", + "Defensive", + "Definite", + "Definitive", + "Delectable", + "Deliberate", + "Delicate", + "Delicious", + "Delighted", + "Delightful", + "Deluxe", + "Demanding", + "Demonstrative", + "Dental", + "Dependable", + "Dependent", + "Descriptive", + "Deserving", + "Designer", + "Desirable", + "Desired", + "Desirous", + "Destined", + "Detailed", + "Determined", + "Developed", + "Developing", + "Devoted", + "Devotional", + "Devout", + "Dexterous", + "Didactic", + "Different", + "Digital", + "Dignified", + "Diligent", + "Dim", + "Dimpled", + "Diplomatic", + "Direct", + "Disarming", + "Discerning", + "Disciplined", + "Discreet", + "Discrete", + "Disguised", + "Distinct", + "Distinctive", + "Distinguished", + "Distinguishing", + "Diverse", + "Diverting", + "Divine", + "Dominant", + "Doting", + "Doubtless", + "Doughty", + "Downright", + "Drafty", + "Dramatic", + "Dreamy", + "Driven", + "Driving", + "Dry", + "Dual", + "Durable", + "Dutiful", + "Dynamic", + "Dynamite", + "Eager", + "Early", + "Earnest", + "Earthly", + "Earthy", + "Easy", + "Easygoing", + "Ebullient", + "Eclectic", + "Economic", + "Economical", + "Ecstatic", + "Ecumenical", + "Edible", + "Edified", + "Educated", + "Educational", + "Effective", + "Effectual", + "Effervescent", + "Efficient", + "Effortless", + "Elaborate", + "Elastic", + "Elated", + "Elating", + "Elder", + "Elderly", + "Electric", + "Electrifying", + "Eleemosynary", + "Elegant", + "Elemental", + "Elementary", + "Eligible", + "Elliptical", + "Eloquent", + "Embellished", + "Emerging", + "Eminent", + "Emotional", + "Empathetic", + "Empowered", + "Enamored", + "Enchanted", + "Enchanting", + "Encouraged", + "Encouraging", + "Endearing", + "Enduring", + "Energetic", + "Energizing", + "Engaging", + "Enhanced", + "Enjoyable", + "Enlightened", + "Enlightening", + "Enlivened", + "Enlivening", + "Enormous", + "Enough", + "Enriching", + "Enterprising", + "Entertaining", + "Enthralling", + "Enthusiastic", + "Enticing", + "Entire", + "Entrancing", + "Entrepreneurial", + "Epicurean", + "Epideictic", + "Equable", + "Equal", + "Equatorial", + "Equiponderant", + "Equipped", + "Equitable", + "Equivalent", + "Erudite", + "Especial", + "Essential", + "Established", + "Esteemed", + "Esthetic", + "Esthetical", + "Eternal", + "Ethical", + "Euphoric", + "Even", + "Eventful", + "Evergreen", + "Everlasting", + "Evident", + "Evocative", + "Exact", + "Exalted", + "Exceeding", + "Excellent", + "Exceptional", + "Excitable", + "Excited", + "Exciting", + "Executive", + "Exemplary", + "Exhilarating", + "Exotic", + "Expansive", + "Expectant", + "Expeditious", + "Expeditive", + "Expensive", + "Experienced", + "Expert", + "Explorative", + "Expressive", + "Exquisite", + "Extraneous", + "Extraordinary", + "Extroverted", + "Exuberant", + "Exultant", + "Fab", + "Fabulous", + "Facile", + "Factual", + "Facultative", + "Fain", + "Faint", + "Fair", + "Faithful", + "Famed", + "Familial", + "Familiar", + "Family", + "Famous", + "Fancy", + "Fantastic", + "Far", + "Faraway", + "Fascinating", + "Fashionable", + "Fast", + "Fatal", + "Fatherly", + "Faultless", + "Favorable", + "Favored", + "Favorite", + "Fearful", + "Fearless", + "Feasible", + "Fecund", + "Feisty", + "Felicitous", + "Feline", + "Fervent", + "Festal", + "Festive", + "Fetching", + "Fickle", + "Fiery", + "Fine", + "Finer", + "Finest", + "Finished", + "Firm", + "First", + "Firsthand", + "Fit", + "Fitting", + "Fixed", + "Flamboyant", + "Flash", + "Flashy", + "Flat", + "Flavorful", + "Flawed", + "Flawless", + "Fleet", + "Flexible", + "Flickering", + "Flimsy", + "Flippant", + "Flourishing", + "Flowery", + "Fluent", + "Fluffy", + "Fluid", + "Flustered", + "Flying", + "Focused", + "Fond", + "Foremost", + "Foresighted", + "Forgiving", + "Forked", + "Formal", + "Formidable", + "Forthcoming", + "Forthright", + "Fortified", + "Fortuitous", + "Fortunate", + "Forward", + "Foundational", + "Foxy", + "Fragrant", + "Frail", + "Frank", + "Fraternal", + "Frayed", + "Freely", + "Frequent", + "Fresh", + "Friendly", + "Frilly", + "Frisky", + "Front", + "Frosty", + "Frozen", + "Fruitful", + "Fulfilled", + "Fulfilling", + "Full", + "Fumbling", + "Fun", + "Functional", + "Funny", + "Fussy", + "Futuristic", + "Fuzzy", + "Gabby", + "Gainful", + "Gallant", + "Galore", + "Game", + "Gamesome", + "Gamy", + "Gaping", + "Gaseous", + "Gaudy", + "General", + "Generous", + "Genial", + "Genteel", + "Gentle", + "Genuine", + "Germane", + "Gettable", + "Giant", + "Giddy", + "Gifted", + "Gigantic", + "Giving", + "Glad", + "Glamorous", + "Glaring", + "Glass", + "Gleaming", + "Gleeful", + "Glib", + "Glistening", + "Glittering", + "Glorious", + "Glossy", + "Glowing", + "Glum", + "Gnarly", + "Godly", + "Golden", + "Good", + "Goodhearted", + "Goodly", + "Goofy", + "Gorgeous", + "Graced", + "Graceful", + "Gracile", + "Gracious", + "Gradely", + "Graithly", + "Grand", + "Grandiose", + "Granular", + "Grateful", + "Gratified", + "Gratifying", + "Gray", + "Greasy", + "Great", + "Greatest", + "Greathearted", + "Green", + "Gregarious", + "Grey", + "Gripping", + "Groovy", + "Grounded", + "Growing", + "Grown", + "Grubby", + "Grumpy", + "Guaranteed", + "Guarded", + "Gubernatorial", + "Guided", + "Guiding", + "Guileless", + "Guiltless", + "Guilty", + "Gullible", + "Gummy", + "Gumptious", + "Gustatory", + "Gusty", + "Gutsy", + "Gymnastic", + "Hairy", + "Halcyon", + "Hale", + "Half", + "Hallowed", + "Handmade", + "Handsome", + "Handy", + "Happening", + "Happy", + "Hardy", + "Harmless", + "Harmonious", + "Harsh", + "Hasty", + "Head", + "Healing", + "Healthful", + "Healthy", + "Heartfelt", + "Hearty", + "Heavenly", + "Heavy", + "Heedful", + "Hefty", + "Hegemonic", + "Helpful", + "Hep", + "Heralded", + "Heroic", + "Heteroclite", + "Heuristic", + "High", + "Highest", + "Hilarious", + "Hip", + "Holy", + "Homely", + "Honest", + "Honeyed", + "Honorable", + "Honorary", + "Honored", + "Hopeful", + "Hortative", + "Hospitable", + "Hot", + "Hotshot", + "Huge", + "Humane", + "Humanitarian", + "Humble", + "Humming", + "Humongous", + "Humorous", + "Hungry", + "Husky", + "Hygienic", + "Icy", + "Ideal", + "Idealistic", + "Identical", + "Idiosyncratic", + "Idolized", + "Ignorant", + "Illimitable", + "Illuminated", + "Illuminating", + "Illustrious", + "Imaginary", + "Imaginative", + "Imitable", + "Immaculate", + "Immaterial", + "Immeasurable", + "Immediate", + "Immense", + "Immortal", + "Immune", + "Impartial", + "Impassioned", + "Impeccable", + "Impeccant", + "Imperturbable", + "Important", + "Impossible", + "Impractical", + "Impressionable", + "Impressive", + "Improbable", + "Improved", + "Improving", + "Improvisational", + "Inborn", + "Incisive", + "Included", + "Inclusive", + "Incomparable", + "Inconsequential", + "Incontestable", + "Incontrovertible", + "Incredible", + "Inculpable", + "Indefatigable", + "Indelible", + "Independent", + "Indestructible", + "Indispensable", + "Indisputable", + "Individual", + "Individualistic", + "Indivisible", + "Indomitable", + "Indubitable", + "Industrious", + "Inerrant", + "Inexhaustible", + "Infallible", + "Infamous", + "Infant", + "Infantile", + "Infatuated", + "Inferior", + "Infinite", + "Influential", + "Informal", + "Informative", + "Informed", + "Ingenious", + "Inimitable", + "Initiate", + "Initiative", + "Innocent", + "Innovative", + "Innoxious", + "Inquisitive", + "Insightful", + "Insignificant", + "Insistent", + "Inspired", + "Inspiring", + "Inspiriting", + "Instantaneous", + "Instinctive", + "Instructive", + "Instrumental", + "Integral", + "Integrated", + "Intellectual", + "Intelligent", + "Intense", + "Intent", + "Intentional", + "Interactive", + "Interconnected", + "Interested", + "Interesting", + "Internal", + "International", + "Intertwined", + "Intimate", + "Intoxicating", + "Intrepid", + "Intriguing", + "Introducer", + "Inventive", + "Invigorated", + "Invigorating", + "Invincible", + "Inviolable", + "Inviting", + "Irrefragable", + "Irrefutable", + "Irreplaceable", + "Irrepressible", + "Irreproachable", + "Irresistible", + "Jaculable", + "Jaded", + "Jaunty", + "Jazzed", + "Jazzy", + "Jessant", + "Jestful", + "Jesting", + "Jeweled", + "Jiggish", + "Jigjog", + "Jimp", + "Jittery", + "Jobbing", + "Jocose", + "Jocoserious", + "Jocular", + "Joculatory", + "Jocund", + "Joint", + "Jointed", + "Jolly", + "Jovial", + "Joyful", + "Joyous", + "Joysome", + "Jubilant", + "Judicious", + "Juicy", + "Julie", + "Jumbled", + "Jumbo", + "Jump", + "Jumpy", + "Junior", + "Just", + "Justified", + "Juvenile", + "Kaleidoscopic", + "Keen", + "Kempt", + "Key", + "Kind", + "Kindhearted", + "Kindly", + "Kindred", + "Kinetic", + "Kingly", + "Knightly", + "Knobby", + "Knotty", + "Knowable", + "Knowing", + "Knowledgeable", + "Known", + "Kooky", + "Kosher", + "Ladylike", + "Large", + "Last", + "Lasting", + "Latitudinarian", + "Laudable", + "Laureate", + "Lavish", + "Lawful", + "Leading", + "Leafy", + "Learned", + "Legal", + "Legendary", + "Legible", + "Legit", + "Legitimate", + "Leisured", + "Leisurely", + "Lenien", + "Leonine", + "Lepid", + "Lettered", + "Liberal", + "Liberated", + "Liberating", + "Light", + "Lighthearted", + "Lightly", + "Likable", + "Like", + "Liked", + "Likely", + "Limber", + "Limited", + "Linear", + "Lined", + "Lionhearted", + "Liquid", + "Literary", + "Literate", + "Lithe", + "Lithesome", + "Little", + "Live", + "Lively", + "Livid", + "Logical", + "Long", + "Lordly", + "Loud", + "Lovable", + "Loved", + "Lovely", + "Loving", + "Low", + "Loyal", + "Lucent", + "Lucid", + "Lucky", + "Lucrative", + "Lumbering", + "Luminous", + "Luscious", + "Lush", + "Lustrous", + "Luxuriant", + "Luxurious", + "Made", + "Magical", + "Magnanimous", + "Magnetic", + "Magnificent", + "Main", + "Majestic", + "Major", + "Malleable", + "Manageable", + "Managerial", + "Manifest", + "Mannerly", + "Marked", + "Marvelous", + "Massive", + "Master", + "Masterful", + "Masterly", + "Matchless", + "Maternal", + "Mature", + "Maturing", + "Maximal", + "Meager", + "Mealy", + "Meaningful", + "Measly", + "Mediate", + "Medical", + "Meditative", + "Medium", + "Mellow", + "Melodic", + "Melodious", + "Memorable", + "Merciful", + "Meritable", + "Meritorious", + "Merry", + "Mesmerizing", + "Metallic", + "Metaphysical", + "Meteoric", + "Methodical", + "Meticulous", + "Mettlesome", + "Mighty", + "Mild", + "Milky", + "Mindful", + "Mindless", + "Miniature", + "Minikin", + "Ministerial", + "Mint", + "Minty", + "Miraculous", + "Mirthful", + "Misty", + "Mitigative", + "Mitigatory", + "Mixed", + "Model", + "Modern", + "Modernistic", + "Modest", + "Moist", + "Momentous", + "Moneyed", + "Monthly", + "Monumental", + "Moral", + "Mortified", + "Motherly", + "Motionless", + "Motivated", + "Motivating", + "Motivational", + "Motor", + "Mountainous", + "Moving", + "Multicolored", + "Multidimensional", + "Multidisciplined", + "Multifaceted", + "Mundane", + "Munificent", + "Muscular", + "Musical", + "Mutual", + "Mysterious", + "Narrow", + "National", + "Nationwide", + "Native", + "Natty", + "Natural", + "Nautical", + "Near", + "Nearby", + "Neat", + "Necessary", + "Needed", + "Negligible", + "Neighboring", + "Neighborly", + "Neoteric", + "Nestling", + "New", + "Newborn", + "Next", + "Nice", + "Nifty", + "Nimble", + "Nippy", + "Noble", + "Nocturnal", + "Noetic", + "Nonchalant", + "Nonpareil", + "Nonstop", + "Normal", + "Notable", + "Noted", + "Noteworthy", + "Noticeable", + "Nourished", + "Nourishing", + "Novel", + "Nubile", + "Nutrimental", + "Nutritious", + "Obedient", + "Objective", + "Obliging", + "Oblong", + "Observant", + "Obtainable", + "Obvious", + "Occasional", + "Oecumenical", + "Official", + "Okay", + "Olympian", + "Onward", + "Open", + "Operative", + "Opportune", + "Optimal", + "Optimistic", + "Optimum", + "Opulent", + "Orange", + "Orderly", + "Ordinary", + "Organic", + "Organized", + "Oriented", + "Original", + "Ornamental", + "Ornate", + "Ornery", + "Outgoing", + "Outlandish", + "Outlying", + "Outrageous", + "Outstanding", + "Oval", + "Overflowing", + "Overjoyed", + "Overriding", + "Overt", + "Palatable", + "Pally", + "Palpable", + "Paradisiac", + "Paradisiacal", + "Parallel", + "Paramount", + "Parched", + "Parental", + "Parnassian", + "Partial", + "Participant", + "Participative", + "Particular", + "Partisan", + "Passionate", + "Pastel", + "Paternal", + "Patient", + "Peaceable", + "Peaceful", + "Peachy", + "Peerless", + "Penetrating", + "Peppery", + "Peppy", + "Perceptive", + "Perfect", + "Perfumed", + "Periodic", + "Perky", + "Permanent", + "Permissive", + "Perseverant", + "Persevering", + "Persistent", + "Personable", + "Personal", + "Perspective", + "Perspicacious", + "Perspicuous", + "Persuasive", + "Pert", + "Pertinent", + "Petite", + "Phenomenal", + "Philanthropic", + "Philoprogenitive", + "Philosophical", + "Physical", + "Picked", + "Picturesque", + "Piercing", + "Pierian", + "Pilot", + "Pink", + "Pioneering", + "Pious", + "Piquant", + "Pithy", + "Pivotal", + "Placid", + "Plain", + "Plaintive", + "Plastic", + "Plausible", + "Playful", + "Pleasant", + "Pleased", + "Pleasing", + "Pleasurable", + "Plenary", + "Plenteous", + "Plentiful", + "Pliable", + "Plucky", + "Plummy", + "Plump", + "Plush", + "Poetic", + "Poignant", + "Pointed", + "Poised", + "Polished", + "Polite", + "Political", + "Popular", + "Portly", + "Posh", + "Positive", + "Possible", + "Potable", + "Potent", + "Potential", + "Powerful", + "Practicable", + "Practical", + "Practised", + "Pragmatic", + "Praiseworthy", + "Prayerful", + "Precious", + "Precise", + "Predominant", + "Preeminent", + "Preferable", + "Preferred", + "Premier", + "Premium", + "Prepared", + "Preponderant", + "Prepotent", + "Present", + "Prestigious", + "Pretty", + "Prevailing", + "Prevalent", + "Prevenient", + "Previous", + "Primal", + "Primary", + "Prime", + "Primed", + "Primo", + "Princely", + "Principled", + "Pristine", + "Private", + "Privileged", + "Prize", + "Prized", + "Prizewinning", + "Pro", + "Proactive", + "Probable", + "Probative", + "Procurable", + "Prodigious", + "Productive", + "Professional", + "Proficient", + "Profitable", + "Profound", + "Profuse", + "Progressive", + "Prolific", + "Prominent", + "Promising", + "Prompt", + "Proper", + "Propertied", + "Prophetic", + "Propitious", + "Prospective", + "Prosperous", + "Protean", + "Protective", + "Proud", + "Provocative", + "Prudent", + "Puissant", + "Pulchritudinous", + "Punchy", + "Punctilious", + "Punctual", + "Pungent", + "Pure", + "Purple", + "Purposeful", + "Quaint", + "Qualified", + "Qualitative", + "Quality", + "Quantifiable", + "Quarterly", + "Queenly", + "Questionable", + "Quick", + "Quiet", + "Quietsome", + "Quintessential", + "Quirky", + "Quiver", + "Quixotic", + "Quizzical", + "Quotable", + "Racy", + "Rad", + "Radiant", + "Rapid", + "Rapturous", + "Rare", + "Rational", + "Raw", + "Reachable", + "Ready", + "Real", + "Realistic", + "Realizable", + "Reasonable", + "Reassuring", + "Recent", + "Receptive", + "Recherche", + "Recipient", + "Reciprocal", + "Recognizable", + "Recognized", + "Recommendable", + "Rectangular", + "Recuperative", + "Red", + "Refined", + "Reflecting", + "Reflective", + "Refreshing", + "Refulgent", + "Regal", + "Regnant", + "Regular", + "Rejuvenescent", + "Relaxed", + "Relevant", + "Reliable", + "Relieved", + "Remarkable", + "Remissive", + "Remote", + "Renowned", + "Repentant", + "Reputable", + "Required", + "Resilient", + "Resolute", + "Resolved", + "Resounding", + "Resourceful", + "Respectable", + "Respectful", + "Resplendent", + "Responsible", + "Responsive", + "Restful", + "Restorative", + "Retentive", + "Revealing", + "Revered", + "Reverent", + "Revitalizing", + "Revolutionary", + "Revolving", + "Rewardable", + "Rewarding", + "Rhapsodic", + "Rich", + "Right", + "Righteous", + "Rightful", + "Ringed", + "Ripe", + "Risible", + "Robust", + "Rollicking", + "Romantic", + "Rooted", + "Rosy", + "Rotating", + "Round", + "Rounded", + "Rousing", + "Royal", + "Rugged", + "Ruling", + "Runny", + "Rural", + "Saccharine", + "Sacred", + "Sacrosanct", + "Safe", + "Sagacious", + "Sage", + "Saintly", + "Salient", + "Salubrious", + "Salutary", + "Salutiferous", + "Sanctified", + "Sanctimonious", + "Sanctioned", + "Sandy", + "Sane", + "Sanguine", + "Sapid", + "Sapient", + "Sapoforic", + "Sassy", + "Satisfactory", + "Satisfied", + "Satisfying", + "Saucy", + "Saving", + "Savory", + "Savvy", + "Scenic", + "Scented", + "Scholarly", + "Scientific", + "Scintillating", + "Scrumptious", + "Scrupulous", + "Seamless", + "Seasonal", + "Seasoned", + "Second", + "Secondary", + "Secret", + "Secure", + "Sedulous", + "Seemly", + "Select", + "Selfless", + "Sensational", + "Sensible", + "Sensitive", + "Sensuous", + "Sentimental", + "Separate", + "Sequacious", + "Serendipitous", + "Serene", + "Serious", + "Service", + "Settled", + "Several", + "Severe", + "Shabby", + "Shadowy", + "Shapely", + "Sharp", + "Shatterproof", + "Sheen", + "Shimmering", + "Shining", + "Shiny", + "Shipshape", + "Shocked", + "Short", + "Showy", + "Shrewd", + "Sightly", + "Significant", + "Silent", + "Silken", + "Silky", + "Silver", + "Silvery", + "Similar", + "Simple", + "Simplistic", + "Sincere", + "Sinewy", + "Single", + "Singular", + "Sisterly", + "Sizable", + "Sizzling", + "Skeletal", + "Skilled", + "Skillful", + "Sleek", + "Slick", + "Slight", + "Slim", + "Slinky", + "Slippery", + "Slow", + "Smacking", + "Small", + "Smart", + "Smashing", + "Smiley", + "Smooth", + "Snap", + "Snappy", + "Snazzy", + "Snod", + "Snoopy", + "Snug", + "Soaring", + "Sociable", + "Social", + "Societal", + "Soft", + "Soigne", + "Solicitous", + "Solid", + "Sonsy", + "Sooth", + "Soothing", + "Sophisticated", + "Soulful", + "Sound", + "Soupy", + "Sour", + "Sovereign", + "Spacious", + "Spangly", + "Spanking", + "Sparkling", + "Sparkly", + "Special", + "Specific", + "Spectacular", + "Specular", + "Speedy", + "Spellbinding", + "Spherical", + "Spicy", + "Spiffy", + "Spirited", + "Spiritual", + "Splendid", + "Splendiferous", + "Spontaneous", + "Sport", + "Sporting", + "Sportive", + "Sporty", + "Spotless", + "Sprightly", + "Spruce", + "Spry", + "Spunky", + "Square", + "Stable", + "Stacked", + "Stainless", + "Stalwart", + "Staminal", + "Standard", + "Standing", + "Star", + "Starchy", + "Stark", + "Starry", + "State", + "Stately", + "Statuesque", + "Staunch", + "Steadfast", + "Steady", + "Steamy", + "Steel", + "Stellar", + "Sterling", + "Sthenic", + "Stimulant", + "Stimulating", + "Stimulative", + "Stipendiary", + "Stirred", + "Stirring", + "Stocky", + "Stoical", + "Storied", + "Stout", + "Stouthearted", + "Straightforward", + "Strange", + "Strapping", + "Strategic", + "Streetwise", + "Strenuous", + "Strict", + "Strident", + "Striking", + "Striped", + "Strong", + "Studious", + "Stunning", + "Stupendous", + "Sturdy", + "Stylish", + "Suasive", + "Suave", + "Sublime", + "Substant", + "Substantial", + "Substantive", + "Subtle", + "Suburban", + "Successful", + "Succinct", + "Succulent", + "Sufficient", + "Sugary", + "Suitable", + "Sultry", + "Summary", + "Summery", + "Sumptuous", + "Sunny", + "Super", + "Superabundant", + "Superb", + "Supereminent", + "Superethical", + "Superexcellent", + "Superficial", + "Superfluous", + "Superior", + "Superlative", + "Supernal", + "Supersonic", + "Supple", + "Supportive", + "Supreme", + "Sure", + "Surpassing", + "Surprised", + "Sustained", + "Svelte", + "Swank", + "Swashbuckling", + "Sweet", + "Swell", + "Swift", + "Swish", + "Sybaritic", + "Sylvan", + "Symmetrical", + "Sympathetic", + "Symphonious", + "Synergistic", + "Systematic", + "Tactful", + "Talented", + "Tall", + "Tame", + "Tan", + "Tangible", + "Tart", + "Tasteful", + "Tasty", + "Teachable", + "Teeming", + "Tempean", + "Temperate", + "Tenable", + "Tenacious", + "Tender", + "Terrific", + "Testimonial", + "Thankful", + "Thankworthy", + "Therapeutic", + "Thorough", + "Those", + "Thoughtful", + "Thrifty", + "Thrilled", + "Thrilling", + "Thriving", + "Tidy", + "Tight", + "Timeless", + "Timely", + "Tinted", + "Tiny", + "Tiptop", + "Tireless", + "Titanic", + "Titillating", + "Today", + "Together", + "Tolerant", + "Top", + "Tops", + "Total", + "Touching", + "Tough", + "Trailblazing", + "Trained", + "Tranquil", + "Transcendent", + "Transcendental", + "Transient", + "Transparent", + "Transpicuous", + "Traveled", + "Treasured", + "Tremendous", + "Triangular", + "Trim", + "Triumphant", + "True", + "Trustful", + "Trusting", + "Trustworthy", + "Trusty", + "Truthful", + "Tubular", + "Tuneful", + "Turgent", + "Twin", + "Tympanic", + "Uber", + "Ultimate", + "Ultra", + "Ultraprecise", + "Unabashed", + "Unadulterated", + "Unaffected", + "Unafraid", + "Unalloyed", + "Unambiguous", + "Unanimous", + "Unarguable", + "Unassuming", + "Unattached", + "Unbeaten", + "Unbelievable", + "Unbiased", + "Unbigoted", + "Unblemished", + "Unbroken", + "Uncommon", + "Uncomplicated", + "Unconditional", + "Unconscious", + "Uncontestable", + "Unconventional", + "Uncorrupted", + "Uncritical", + "Undamaged", + "Undauntable", + "Undaunted", + "Undefeated", + "Undefiled", + "Undeniable", + "Understandable", + "Understanding", + "Understated", + "Understood", + "Undesigning", + "Undiminished", + "Undisputed", + "Undivided", + "Undoubted", + "Unencumbered", + "Unequaled", + "Unequalled", + "Unequivocal", + "Unerring", + "Unfailing", + "Unfaltering", + "Unfaultable", + "Unfeigned", + "Unfettered", + "Unflagging", + "Unflappable", + "Ungrudging", + "Unhampered", + "Unharmed", + "Unhesitating", + "Unhurt", + "Unified", + "Uniform", + "Unimpaired", + "Unimpeachable", + "Unimpeded", + "Unique", + "United", + "Universal", + "Unlimited", + "Unmistakable", + "Unmitigated", + "Unobjectionable", + "Unobstructed", + "Unobtrusive", + "Unopposed", + "Unpretentious", + "Unquestionable", + "Unrefuted", + "Unreserved", + "Unrivalled", + "Unruffled", + "Unselfish", + "Unshakable", + "Unshaken", + "Unspoiled", + "Unspoilt", + "Unstoppable", + "Unsullied", + "Unsurpassed", + "Untarnished", + "Untiring", + "Untouched", + "Untroubled", + "Ununprejudiced", + "Unusual", + "Unwavering", + "Upbeat", + "Upcoming", + "Uplifted", + "Uplifting", + "Uppermost", + "Upright", + "Upset", + "Upstanding", + "Upward", + "Upwardly", + "Urban", + "Urbane", + "Usable", + "Useful", + "Useless", + "Utmost", + "Vacant", + "Vain", + "Valiant", + "Valid", + "Validatory", + "Valorous", + "Valuable", + "Valued", + "Vapid", + "Variable", + "Vast", + "Vaulting", + "Vehement", + "Velvety", + "Venerable", + "Venerated", + "Venturesome", + "Venue", + "Veracious", + "Verdurous", + "Veridical", + "Verifiable", + "Verified", + "Versatile", + "Versed", + "Vestal", + "Veteran", + "Viable", + "Vibrant", + "Vibratile", + "Victor", + "Victorious", + "Vigilant", + "Vigorous", + "Violet", + "Virile", + "Virtual", + "Virtuous", + "Visible", + "Visionary", + "Vital", + "Vivacious", + "Vivid", + "Vocal", + "Volant", + "Volitional", + "Voluminous", + "Voluptuous", + "Vulnerary", + "Wanted", + "Warm", + "Warmhearted", + "Warranted", + "Wasteful", + "Watchful", + "Waterlogged", + "Watery", + "Wavy", + "Wealthy", + "Weekly", + "Weighty", + "Welcome", + "Welcomed", + "Welcoming", + "Weleful", + "Welfaring", + "Well", + "Welsome", + "Wet", + "Whimsical", + "Whole", + "Wholehearted", + "Wholesome", + "Whopping", + "Wide", + "Wild", + "Willed", + "Willing", + "Winding", + "Windy", + "Winged", + "Winning", + "Winsome", + "Wired", + "Wise", + "Witty", + "Wizard", + "Wizardly", + "Wobbly", + "Wonderful", + "Wondrous", + "Wooden", + "Wordy", + "Workable", + "Worldly", + "Worshipful", + "Worth", + "Worthwhile", + "Worthy", + "Xenial", + "Xenodochial", + "Yearly", + "Yern", + "Young", + "Youthful", + "Yummy", + "Zaftig", + "Zany", + "Zappy", + "Zazzy", + "Zealand", + "Zealful", + "Zealous", + "Zestful", + "Zesty", + "Zigzag", + "Zingy", + "Zippy", + "Zootrophic", + "Zooty" + ] + +nouns :: [Text] +nouns = + [ "Academician", + "Acceptor", + "Access", + "Acclaim", + "Accolade", + "Account", + "Accuracy", + "Ace", + "Achiever", + "Acumen", + "Addition", + "Adherent", + "Adjutant", + "Administrator", + "Admirer", + "Adorer", + "Advantage", + "Aesthete", + "Aficionada", + "Aficionado", + "Agent", + "Aide", + "Almsgiver", + "Altruist", + "Ambassador", + "Amity", + "Angel", + "Apostle", + "Appreciator", + "Arbiter", + "Archetype", + "Architect", + "Artisan", + "Artist", + "Artiste", + "Asset", + "Assignee", + "Assigner", + "Athlete", + "Author", + "Authority", + "Avowal", + "Awardee", + "Aye", + "Azure", + "Baby", + "Backer", + "Backup", + "Beatitude", + "Beauty", + "Begetter", + "Being", + "Believer", + "Benchmark", + "Benefaction", + "Benefactor", + "Benefactress", + "Beneficiary", + "Benefit", + "Bestower", + "Betterment", + "Bigwig", + "Blessing", + "Bliss", + "Bloom", + "Blossom", + "Blossoming", + "Bodyguard", + "Bonanza", + "Bonus", + "Boost", + "Booster", + "Boss", + "Bound", + "Bounty", + "Brain", + "Brass", + "Brief", + "Brother", + "Buddy", + "Builder", + "Calm", + "Campaigner", + "Capital", + "Captain", + "Care", + "Caretaker", + "Catalyst", + "Cause", + "Celebrant", + "Celebrator", + "Celestial", + "Chair", + "Chairperson", + "Chamberlain", + "Champ", + "Champion", + "Charity", + "Charmer", + "Cheer", + "Cheers", + "Chief", + "Chieftain", + "Chirpy", + "Choice", + "Chortle", + "Chosen", + "Chuckle", + "Chum", + "Cinch", + "Civility", + "Clairvoyant", + "Classic", + "Clear", + "Climb", + "Climber", + "Climbing", + "Close", + "Closing", + "Coadjutant", + "Coadjutor", + "Coequal", + "Coiner", + "Collaborator", + "Colleague", + "Collector", + "Comfort", + "Comforter", + "Comic", + "Commander", + "Commendatory", + "Compassion", + "Composer", + "Comrade", + "Concierge", + "Condolence", + "Conductor", + "Confederate", + "Confidant", + "Confidence", + "Connoisseur", + "Consciousness", + "Conservator", + "Consoler", + "Constant", + "Constitutional", + "Consul", + "Consultant", + "Contemporary", + "Content", + "Contributor", + "Controller", + "Conversant", + "Cooperator", + "Cope", + "Cornerstone", + "Councillor", + "Counselor", + "Courage", + "Crack", + "Crackerjack", + "Craftsperson", + "Creator", + "Credential", + "Credit", + "Curator", + "Custodian", + "Dainty", + "Dancer", + "Daring", + "Darling", + "Dean", + "Dear", + "Decency", + "Deep", + "Defender", + "Definite", + "Delight", + "Demulcent", + "Deserving", + "Designer", + "Devisee", + "Devisor", + "Devotee", + "Devotional", + "Devout", + "Didactic", + "Director", + "Disciple", + "Discoverer", + "Distributor", + "Doer", + "Doll", + "Donee", + "Donor", + "Doting", + "Doyen", + "Doyenne", + "Dreamboat", + "Dynamic", + "Dynamite", + "Dynamo", + "Earnest", + "Ease", + "Effect", + "Efficiency", + "Efficient", + "Einstein", + "Elder", + "Eligible", + "Employer", + "Enchanter", + "Enchantress", + "Encourager", + "Endorser", + "Enlivening", + "Enough", + "Entertainer", + "Enthusiast", + "Entrepreneur", + "Epicure", + "Epicurean", + "Epitome", + "Equal", + "Equity", + "Equivalent", + "Essence", + "Essential", + "Esteem", + "Eternal", + "Ethic", + "Example", + "Exclusive", + "Executive", + "Exemplar", + "Exemplary", + "Experimenter", + "Expert", + "Exponent", + "Eyes", + "Fair", + "Faith", + "Faithful", + "Fame", + "Familiar", + "Fancier", + "Fancy", + "Fantastic", + "Fare", + "Fascinator", + "Fashioner", + "Favor", + "Favorite", + "Favour", + "Felicity", + "Fine", + "Fireball", + "Firm", + "First", + "Fleet", + "Folks", + "Foodie", + "Forbear", + "Force", + "Forefather", + "Foreman", + "Forerunner", + "Foresight", + "Forever", + "Forgiveness", + "Fortunate", + "Fortune", + "Forward", + "Foundation", + "Founder", + "Fountain", + "Fountainhead", + "Freedom", + "Freethinking", + "Fresh", + "Friend", + "Friendly", + "Fulfilling", + "Full", + "Fun", + "Funny", + "Gag", + "Gaiety", + "Gain", + "Gala", + "Galahad", + "Gale", + "Gallant", + "Gallantry", + "Game", + "Garb", + "Garden", + "Garland", + "Garnish", + "Gastronome", + "Gastronomy", + "Gather", + "Gathering", + "Geek", + "Gem", + "Generativity", + "Generator", + "Generosity", + "Genius", + "Gentle", + "Gentlefolk", + "Gentleman", + "Gentlewoman", + "Gift", + "Gild", + "Gilt", + "Girlfriend", + "Gist", + "Giver", + "Giving", + "Glamour", + "Glance", + "Glare", + "Glaze", + "Gleam", + "Gleaming", + "Glimmer", + "Glimmering", + "Glint", + "Glisten", + "Glister", + "Glitterati", + "Glitz", + "Glory", + "Gloss", + "Glossy", + "Goal", + "God", + "Goddess", + "Godparent", + "Godsend", + "Golconda", + "Gold", + "Good", + "Goodness", + "Goodwill", + "Goody", + "Gorgeousness", + "Gourmet", + "Governor", + "Grace", + "Gracility", + "Graciousness", + "Grade", + "Graduate", + "Grammy", + "Grandee", + "Grandeur", + "Grandmaster", + "Grant", + "Grantee", + "Grantor", + "Grass", + "Gratefulness", + "Gratification", + "Gratitude", + "Great", + "Greatness", + "Greeting", + "Grin", + "Grit", + "Groove", + "Growing", + "Growth", + "Grubstake", + "Guarantor", + "Guard", + "Guardian", + "Guest", + "Guffaw", + "Guidance", + "Guide", + "Gumption", + "Guru", + "Gush", + "Gusto", + "Gut", + "Gymnastic", + "Hale", + "Handler", + "Happening", + "Harmony", + "Head", + "Headman", + "Heart", + "Heartthrob", + "Heaven", + "Height", + "Heir", + "Heiress", + "Hello", + "Help", + "Helper", + "Helpmate", + "Heritor", + "Heritress", + "Heritrix", + "Hero", + "Heroine", + "Heuristic", + "Highflier", + "Hilarity", + "Holy", + "Honesty", + "Honor", + "Hooray", + "Hope", + "Hopeful", + "Hotshot", + "Humanitarian", + "Humor", + "Husband", + "Icon", + "Idea", + "Ideal", + "Idol", + "Idolizer", + "Improvement", + "Inamorata", + "Inamorato", + "Increase", + "Independent", + "Indivisible", + "Industrialist", + "Infant", + "Infinite", + "Ingenuity", + "Inheritor", + "Initiative", + "Initiator", + "Innocent", + "Innovator", + "Inspiration", + "Institutor", + "Integral", + "Intellect", + "Intent", + "Interest", + "Intimate", + "Inventor", + "Invitation", + "Invite", + "Invitee", + "Jest", + "Jester", + "Jingle", + "Joker", + "Josh", + "Jubilation", + "Juggler", + "Justice", + "Keeper", + "Key", + "Kindred", + "Kingpin", + "Kiss", + "Knight", + "Lady", + "Lark", + "Lasting", + "Latitudinarian", + "Laugh", + "Laureate", + "Lead", + "Leader", + "Learning", + "Legatee", + "Legator", + "Legend", + "Legislator", + "Leisure", + "Liberation", + "Libertarian", + "Lieutenant", + "Life", + "Light", + "Like", + "Liking", + "Lionheart", + "Literate", + "Lord", + "Love", + "Lover", + "Luck", + "Lust", + "Luster", + "Lustre", + "Luxury", + "Maestro", + "Magician", + "Magistrate", + "Magnitude", + "Majesty", + "Major", + "Majority", + "Make", + "Maker", + "Mana", + "Manager", + "Manner", + "Marvel", + "Massage", + "Mastermind", + "Mate", + "Matriarch", + "Matter", + "Mentor", + "Mercy", + "Merit", + "Method", + "Might", + "Minder", + "Minikin", + "Mint", + "Miracle", + "Mirth", + "Model", + "Modern", + "Mom", + "Moppet", + "Morale", + "Most", + "Motivator", + "Motor", + "Mover", + "Moving", + "Much", + "Multitude", + "Music", + "Nabit", + "Narration", + "Narrator", + "Nascency", + "Natation", + "National", + "Native", + "Natural", + "Negoce", + "Neighbor", + "Neonate", + "Neoteric", + "Nestling", + "Newborn", + "Nicety", + "Nimblewit", + "Nipper", + "Nirvana", + "Noble", + "Nobleman", + "Nod", + "Nonpareil", + "Noon", + "Notable", + "Note", + "Notice", + "Novel", + "Nudge", + "Nurse", + "Nursling", + "Nurture", + "Objective", + "Offer", + "Officer", + "Official", + "Offspring", + "Olympian", + "One", + "Oodles", + "Oomph", + "Ooze", + "Operator", + "Opportunity", + "Optimist", + "Optimum", + "Orchestrator", + "Organizer", + "Original", + "Originator", + "Ornamental", + "Overflowing", + "Overseer", + "Owner", + "Pacifist", + "Pal", + "Paradigm", + "Paradise", + "Paragon", + "Paramount", + "Pard", + "Pardon", + "Parent", + "Participant", + "Particular", + "Partisan", + "Partner", + "Passion", + "Pathfinder", + "Patience", + "Patient", + "Patriarch", + "Patron", + "Peace", + "Peacekeeper", + "Peach", + "Peak", + "Pearl", + "Peer", + "Pep", + "Perfection", + "Perfectionist", + "Performer", + "Personality", + "Perspective", + "Pet", + "Phenomenon", + "Philanthropist", + "Philanthropy", + "Philosopher", + "Pick", + "Pilot", + "Pioneer", + "Pivot", + "Plan", + "Planner", + "Play", + "Player", + "Playmate", + "Pleasing", + "Pleasure", + "Plenitude", + "Plenty", + "Poet", + "Poise", + "Positive", + "Possessor", + "Possible", + "Postulant", + "Potential", + "Pragmatic", + "Praise", + "Prayer", + "Precious", + "Preemption", + "Premier", + "Premium", + "Presence", + "Present", + "Presenter", + "President", + "Pretty", + "Primary", + "Prime", + "Primogenitor", + "Prince", + "Princess", + "Principal", + "Prize", + "Pro", + "Proconsul", + "Procreator", + "Prodigy", + "Produce", + "Producer", + "Professional", + "Professor", + "Progenitor", + "Progeny", + "Progress", + "Progressive", + "Prolepsis", + "Promoter", + "Promotion", + "Promulgator", + "Prophet", + "Proponent", + "Proposer", + "Proprietor", + "Prospective", + "Protagonist", + "Protector", + "Protege", + "Provider", + "Provocative", + "Publisher", + "Purveyor", + "Quaff", + "Quaintise", + "Quaintisiness", + "Quality", + "Quantity", + "Quarter", + "Queen", + "Queenhood", + "Querist", + "Quest", + "Quester", + "Question", + "Quick", + "Quickness", + "Quickstep", + "Quiet", + "Quillet", + "Quintessence", + "Quip", + "Quirk", + "Quotation", + "Radiant", + "Rapture", + "Rational", + "Ready", + "Real", + "Reason", + "Receiver", + "Reception", + "Reciprocal", + "Recuperation", + "Regulator", + "Rejoicing", + "Rejuvenation", + "Release", + "Relief", + "Reliever", + "Renovation", + "Reputation", + "Resource", + "Respect", + "Restoration", + "Result", + "Reverence", + "Reward", + "Rhapsody", + "Rich", + "Right", + "Rise", + "Roll", + "Romantic", + "Romeo", + "Rooter", + "Rose", + "Round", + "Rouse", + "Ruler", + "Ruling", + "Run", + "Runner", + "Sage", + "Saint", + "Salient", + "Salubrity", + "Salute", + "Sanctity", + "Sanctuary", + "Satisfaction", + "Savant", + "Saver", + "Savior", + "Savory", + "Savvy", + "Scholar", + "Scholarship", + "Science", + "Scion", + "Script", + "Scripter", + "Sculptor", + "Seeker", + "Select", + "Sensation", + "Sense", + "Sensibility", + "Sentiment", + "Sentinel", + "Serendipity", + "Serene", + "Serenity", + "Server", + "Set", + "Settling", + "Shape", + "Share", + "Sharp", + "Sharpy", + "Shaver", + "Shelter", + "Shine", + "Show", + "Sight", + "Significance", + "Significant", + "Simplicity", + "Sinew", + "Sir", + "Sire", + "Sister", + "Size", + "Skill", + "Skin", + "Skipper", + "Sleek", + "Slick", + "Smash", + "Smile", + "Smooth", + "Smoothie", + "Snap", + "Snuggle", + "Soar", + "Sociable", + "Social", + "Socializer", + "Sol", + "Solid", + "Sophisticate", + "Soul", + "Sovereign", + "Spark", + "Sparkling", + "Special", + "Specialist", + "Spectacle", + "Spectacular", + "Speed", + "Spell", + "Spice", + "Spirit", + "Spiritual", + "Splendor", + "Sponsor", + "Sport", + "Sportsmanship", + "Spot", + "Sprite", + "Sprout", + "Squire", + "Stalwart", + "Standard", + "Star", + "State", + "Steady", + "Steward", + "Stipend", + "Stipendiary", + "Stir", + "Stirring", + "Strategist", + "Strategy", + "Stripling", + "Strive", + "Student", + "Style", + "Stylist", + "Sublime", + "Substance", + "Success", + "Successor", + "Sufficiency", + "Sugar", + "Suitor", + "Summary", + "Summer", + "Super", + "Superintendent", + "Superior", + "Superlative", + "Superman", + "Supervisor", + "Superwoman", + "Supplier", + "Supply", + "Support", + "Surety", + "Surprise", + "Survivor", + "Swain", + "Sweetheart", + "Sweetness", + "Swell", + "Sympathy", + "Synergy", + "System", + "Tact", + "Talent", + "Taste", + "Teacher", + "Teaching", + "Team", + "Teammate", + "Tender", + "Testament", + "Testator", + "Testimonial", + "Testimony", + "Thank", + "Thanksgiving", + "Therapy", + "Thinker", + "Thrill", + "Timesaver", + "Tiptop", + "Titleholder", + "Today", + "Tootsie", + "Top", + "Tot", + "Total", + "Touch", + "Tout", + "Trailblazer", + "Training", + "Tranquillity", + "Transcendent", + "Transient", + "Travel", + "Treasure", + "Trim", + "Triumph", + "Trust", + "Trustee", + "Trusty", + "Truth", + "Try", + "Tuition", + "Tune", + "Tutor", + "Tycoon", + "Uberty", + "Ubiquitary", + "Ultimate", + "Ultimation", + "Understanding", + "Underwriter", + "Unique", + "Universal", + "Upholder", + "Uplift", + "Valedictorian", + "Valiant", + "Validator", + "Valuable", + "Value", + "Vast", + "Vaulter", + "Veracity", + "Vestal", + "Veteran", + "Virtue", + "Virtuosity", + "Visionary", + "Visitor", + "Vitality", + "Vogue", + "Votary", + "Warden", + "Warmth", + "Warrantee", + "Warrantor", + "Welcome", + "Well", + "Wellspring", + "Whip", + "Whiz", + "Whole", + "Wife", + "Will", + "Willpower", + "Win", + "Winner", + "Winning", + "Wisdom", + "Wise", + "Wizard", + "Wonder", + "Wonderment", + "Wooer", + "Wordsmith", + "Workhorse", + "Workmate", + "Worshipper", + "Worth", + "Worthy", + "Writer", + "Wunderkind", + "X", + "Xenagogue", + "Xenium", + "Xenophile", + "Xenophilia", + "Xesturgy", + "Xfactor", + "Yard", + "Yeve", + "Yield", + "Yift", + "Yoke", + "Young", + "Youngster", + "Youth", + "Zaniness", + "Zarf", + "Zeal", + "Zegedine", + "Zest", + "Zibeline", + "Zing" + ] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 8b6a402288..3a957fbe2b 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -124,7 +124,7 @@ data ChatMsgEvent | XInfo Profile | XContact Profile (Maybe XContactId) | XGrpInv GroupInvitation - | XGrpAcpt MemberId + | XGrpAcpt MemberId (Maybe Profile) | XGrpMemNew MemberInfo | XGrpMemIntro MemberInfo | XGrpMemInv MemberId IntroInvitation @@ -413,7 +413,7 @@ toCMEventTag = \case XInfo _ -> XInfo_ XContact _ _ -> XContact_ XGrpInv _ -> XGrpInv_ - XGrpAcpt _ -> XGrpAcpt_ + XGrpAcpt _ _ -> XGrpAcpt_ XGrpMemNew _ -> XGrpMemNew_ XGrpMemIntro _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -479,7 +479,7 @@ appToChatMessage AppMessage {msgId, event, params} = do XInfo_ -> XInfo <$> p "profile" XContact_ -> XContact <$> p "profile" <*> opt "contactReqId" XGrpInv_ -> XGrpInv <$> p "groupInvitation" - XGrpAcpt_ -> XGrpAcpt <$> p "memberId" + XGrpAcpt_ -> XGrpAcpt <$> p "memberId" <*> opt "memberProfile" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -521,7 +521,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p XInfo profile -> o ["profile" .= profile] XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile] XGrpInv groupInv -> o ["groupInvitation" .= groupInv] - XGrpAcpt memId -> o ["memberId" .= memId] + XGrpAcpt memId profile -> o $ ("memberProfile" .=? profile) ["memberId" .= memId] XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] XGrpMemIntro memInfo -> o ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index de0abc8d21..456bb21634 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -27,6 +27,7 @@ module Simplex.Chat.Store setActiveUser, createDirectConnection, createConnReqConnection, + getProfileById, getConnReqContactXContactId, createDirectContact, getContactGroupNames, @@ -76,16 +77,18 @@ module Simplex.Chat.Store getUserGroups, getUserGroupDetails, getGroupInvitation, - createContactMember, + createNewContactMember, getMemberInvitation, createMemberConnection, updateGroupMemberStatus, + createMemberIncognitoProfile, createNewGroupMember, deleteGroupMember, deleteGroupMemberConnection, createIntroductions, updateIntroStatus, saveIntroInvitation, + getGroupMemberProfileId, createIntroReMember, createIntroToMemberContact, saveMemberInvitation, @@ -223,6 +226,7 @@ import Simplex.Chat.Migrations.M20220626_auto_reply import Simplex.Chat.Migrations.M20220702_calls import Simplex.Chat.Migrations.M20220715_groups_chat_item_id import Simplex.Chat.Migrations.M20220811_chat_items_indices +import Simplex.Chat.Migrations.M20220812_incognito_profiles import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -252,7 +256,8 @@ schemaMigrations = ("20220626_auto_reply", m20220626_auto_reply), ("20220702_calls", m20220702_calls), ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), - ("20220811_chat_items_indices", m20220811_chat_items_indices) + ("20220811_chat_items_indices", m20220811_chat_items_indices), + ("20220812_incognito_profiles", m20220812_incognito_profiles) ] -- | The list of migrations in ascending order by date @@ -302,7 +307,7 @@ createUser db Profile {displayName, fullName, image} activeUser = (profileId, displayName, userId, True, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser (userId, contactId, activeUser, displayName, fullName, image) + pure $ toUser (userId, contactId, profileId, activeUser, displayName, fullName, image) getUsers :: DB.Connection -> IO [User] getUsers db = @@ -310,15 +315,15 @@ getUsers db = <$> DB.query_ db [sql| - SELECT u.user_id, u.contact_id, u.active_user, u.local_display_name, p.full_name, p.image + SELECT u.user_id, u.contact_id, p.contact_profile_id, u.active_user, u.local_display_name, p.full_name, p.image FROM users u JOIN contacts c ON u.contact_id = c.contact_id JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id |] -toUser :: (UserId, Int64, Bool, ContactName, Text, Maybe ImageData) -> User -toUser (userId, userContactId, activeUser, displayName, fullName, image) = - let profile = Profile {displayName, fullName, image} +toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData) -> User +toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image) = + let profile = LocalProfile {profileId, displayName, fullName, image} in User {userId, userContactId, localDisplayName = displayName, profile, activeUser} setActiveUser :: DB.Connection -> UserId -> IO () @@ -326,21 +331,22 @@ setActiveUser db userId = do DB.execute_ db "UPDATE users SET active_user = 0" DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId) -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash xContactId = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash xContactId incognitoProfile = do createdAt <- getCurrentTime + customUserProfileId <- createIncognitoProfile_ db userId createdAt incognitoProfile let pccConnStatus = ConnJoined DB.execute db [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, - created_at, updated_at, via_contact_uri_hash, xcontact_id - ) VALUES (?,?,?,?,?,?,?,?) + via_contact_uri_hash, xcontact_id, custom_user_profile_id, created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?) |] - (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt, cReqHash, xContactId) + (userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId, customUserProfileId, createdAt, createdAt) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, customUserProfileId, createdAt, updatedAt = createdAt} getConnReqContactXContactId :: DB.Connection -> UserId -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId db userId cReqHash = do @@ -356,9 +362,9 @@ getConnReqContactXContactId db userId cReqHash = do [sql| SELECT -- Contact - ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -376,47 +382,72 @@ getConnReqContactXContactId db userId cReqHash = do "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -createDirectConnection :: DB.Connection -> UserId -> ConnId -> ConnStatus -> IO PendingContactConnection -createDirectConnection db userId acId pccConnStatus = do +createDirectConnection :: DB.Connection -> UserId -> ConnId -> ConnStatus -> Maybe Profile -> IO PendingContactConnection +createDirectConnection db userId acId pccConnStatus incognitoProfile = do createdAt <- getCurrentTime + customUserProfileId <- createIncognitoProfile_ db userId createdAt incognitoProfile DB.execute db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_status, conn_type, created_at, updated_at) VALUES (?,?,?,?,?,?) + (user_id, agent_conn_id, conn_status, conn_type, custom_user_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?) |] - (userId, acId, pccConnStatus, ConnContact, createdAt, createdAt) + (userId, acId, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, customUserProfileId, createdAt, updatedAt = createdAt} -createMemberContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createMemberContactConnection_ db userId agentConnId viaContact = createConnection_ db userId ConnContact Nothing agentConnId viaContact Nothing +createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Maybe Profile -> IO (Maybe Int64) +createIncognitoProfile_ db userId createdAt incognitoProfile = + forM incognitoProfile $ \Profile {displayName, fullName, image} -> do + DB.execute + db + [sql| + INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?) + |] + (displayName, fullName, image, userId, Just True, createdAt, createdAt) + insertedRowId db -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createConnection_ db userId connType entityId acId viaContact viaUserContactLink connLevel currentTs = do +getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO Profile +getProfileById db userId profileId = + ExceptT . firstRow toProfile (SEProfileNotFound profileId) $ + DB.query + db + [sql| + SELECT display_name, full_name, image + FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? + |] + (userId, profileId) + where + toProfile :: (ContactName, Text, Maybe ImageData) -> Profile + toProfile (displayName, fullName, image) = Profile {displayName, fullName, image} + +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection +createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do DB.execute db [sql| INSERT INTO connections ( - user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, conn_status, conn_type, + user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, custom_user_profile_id, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, viaUserContactLink, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, customUserProfileId, ConnNew, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, connLevel, connStatus = ConnNew, createdAt = currentTs} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, customUserProfileId, connLevel, connStatus = ConnNew, createdAt = currentTs} where ent ct = if connType == ct then entityId else Nothing createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact db userId activeConn@Connection {connId} profile = do createdAt <- liftIO getCurrentTime - (localDisplayName, contactId, _) <- createContact_ db userId connId profile Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} + (localDisplayName, contactId, profileId) <- createContact_ db userId connId profile Nothing createdAt + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} -createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, Int64, Int64) +createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute @@ -430,7 +461,7 @@ createContact_ db userId connId Profile {displayName, fullName, image} viaGroup (profileId, ldn, userId, viaGroup, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) - pure (ldn, contactId, profileId) + pure . Right $ (ldn, contactId, profileId) getContactGroupNames :: DB.Connection -> UserId -> Contact -> IO [GroupName] getContactGroupNames db userId Contact {contactId} = @@ -482,9 +513,9 @@ deleteContactProfile_ db userId contactId = (userId, contactId) updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () -updateUserProfile db User {userId, userContactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} +updateUserProfile db User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName}} p'@Profile {displayName = newName} | displayName == newName = - liftIO $ updateContactProfile_ db userId userContactId p' + liftIO $ updateContactProfile_ db userId profileId p' | otherwise = checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime @@ -493,49 +524,35 @@ updateUserProfile db User {userId, userContactId, localDisplayName, profile = Pr db "INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" (newName, newName, userId, currentTs, currentTs) - updateContactProfile_' db userId userContactId p' currentTs + updateContactProfile_' db userId profileId p' currentTs updateContact_ db userId userContactId localDisplayName newName currentTs updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact -updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = Profile {displayName}} p'@Profile {displayName = newName} +updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName}} p'@Profile {displayName = newName} | displayName == newName = - liftIO $ updateContactProfile_ db userId contactId p' $> (c :: Contact) {profile = p'} + liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p'} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime - updateContactProfile_' db userId contactId p' currentTs + updateContactProfile_' db userId profileId p' currentTs updateContact_ db userId contactId localDisplayName ldn currentTs - pure $ (c :: Contact) {localDisplayName = ldn, profile = p'} + pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p'} -updateContactProfile_ :: DB.Connection -> UserId -> Int64 -> Profile -> IO () -updateContactProfile_ db userId contactId profile = do +updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () +updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime - updateContactProfile_' db userId contactId profile currentTs + updateContactProfile_' db userId profileId profile currentTs -updateContactProfile_' :: DB.Connection -> UserId -> Int64 -> Profile -> UTCTime -> IO () -updateContactProfile_' db userId contactId Profile {displayName, fullName, image} updatedAt = do - DB.executeNamed +updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () +updateContactProfile_' db userId profileId Profile {displayName, fullName, image} updatedAt = do + DB.execute db [sql| UPDATE contact_profiles - SET display_name = :display_name, - full_name = :full_name, - image = :image, - updated_at = :updated_at - WHERE contact_profile_id IN ( - SELECT contact_profile_id - FROM contacts - WHERE user_id = :user_id - AND contact_id = :contact_id - ) + SET display_name = ?, full_name = ?, image = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? |] - [ ":display_name" := displayName, - ":full_name" := fullName, - ":image" := image, - ":updated_at" := updatedAt, - ":user_id" := userId, - ":contact_id" := contactId - ] + (displayName, fullName, image, updatedAt, userId, profileId) updateContact_ :: DB.Connection -> UserId -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContact_ db userId contactId displayName newName updatedAt = do @@ -549,17 +566,17 @@ updateContact_ db userId contactId displayName newName updatedAt = do (newName, updatedAt, userId, contactId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId) -type ContactRow = (Int64, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, UTCTime, UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, UTCTime, UTCTime) toContact :: ContactRow :. ConnectionRow -> Contact -toContact ((contactId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = - let profile = Profile {displayName, fullName, image} +toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image} activeConn = toConnection connRow in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError ((contactId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = - let profile = Profile {displayName, fullName, image} +toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image} in case toMaybeConnection connRow of Just activeConn -> Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} @@ -586,7 +603,7 @@ createUserContactLink db userId agentConnId cReq = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs getUserContactLinkConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection] getUserContactLinkConnections db user = do @@ -599,7 +616,7 @@ getUserContactLinks db User {userId} = <$> DB.queryNamed db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, uc.user_contact_link_id, uc.conn_req_contact FROM connections c @@ -717,7 +734,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN createContactRequest :: IO (Either StoreError Int64) createContactRequest = do currentTs <- getCurrentTime - withLocalDisplayName db userId displayName (createContactRequest_ currentTs) + withLocalDisplayName db userId displayName (fmap Right . createContactRequest_ currentTs) where createContactRequest_ currentTs ldn = do DB.execute @@ -742,9 +759,9 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN [sql| SELECT -- Contact - ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -777,9 +794,10 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN updateProfile currentTs if displayName == oldDisplayName then Right <$> DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, currentTs, userId, cReqId) - else withLocalDisplayName db userId displayName $ \ldn -> do - DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, ldn, currentTs, userId, cReqId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId) + else withLocalDisplayName db userId displayName $ \ldn -> + Right <$> do + DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, ldn, currentTs, userId, cReqId) + DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId) where updateProfile currentTs = DB.execute @@ -853,17 +871,18 @@ deleteContactRequest db userId contactRequestId = do (userId, userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> Int64 -> Profile -> Int64 -> Maybe XContactId -> IO Contact -createAcceptedContact db userId agentConnId localDisplayName profileId profile userContactLinkId xContactId = do +createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe Profile -> IO Contact +createAcceptedContact db userId agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) - currentTs <- getCurrentTime + createdAt <- getCurrentTime + customUserProfileId <- createIncognitoProfile_ db userId createdAt incognitoProfile DB.execute db "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" - (userId, localDisplayName, profileId, currentTs, currentTs, xContactId) + (userId, localDisplayName, profileId, createdAt, createdAt, xContactId) contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) 0 currentTs - pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, createdAt = currentTs, updatedAt = currentTs} + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt = createdAt, updatedAt = createdAt} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -917,7 +936,7 @@ getPendingContactConnections db User {userId} = do <$> DB.queryNamed db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, created_at, updated_at FROM connections WHERE user_id = :user_id AND conn_type = :conn_type @@ -933,7 +952,7 @@ getContactConnections db userId Contact {contactId} = DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id @@ -945,14 +964,14 @@ getContactConnections db userId Contact {contactId} = type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, ConnStatus, ConnType) :. EntityIdsRow :. Only UTCTime +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Maybe Int64, ConnStatus, ConnType) :. EntityIdsRow :. Only UTCTime -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe ConnStatus, Maybe ConnType) :. EntityIdsRow :. Only (Maybe UTCTime) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Int64, Maybe ConnStatus, Maybe ConnType) :. EntityIdsRow :. Only (Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, customUserProfileId, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) = let entityId = entityId_ connType - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, connStatus, connType, entityId, createdAt} + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, customUserProfileId, connStatus, connType, entityId, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -962,30 +981,25 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, connStat entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just connStatus, Just connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) = - Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, customUserProfileId, Just connStatus, Just connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, customUserProfileId, connStatus, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) toMaybeConnection _ = Nothing getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact] -getMatchingContacts db userId Contact {contactId, profile = Profile {displayName, fullName, image}} = do +getMatchingContacts db userId Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do contactIds <- map fromOnly - <$> DB.queryNamed + <$> DB.query db [sql| SELECT ct.contact_id FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id - WHERE ct.user_id = :user_id AND ct.contact_id != :contact_id - AND p.display_name = :display_name AND p.full_name = :full_name - AND ((p.image IS NULL AND :image IS NULL) OR p.image = :image) + WHERE ct.user_id = ? AND ct.contact_id != ? + AND p.display_name = ? AND p.full_name = ? + AND ((p.image IS NULL AND ? IS NULL) OR p.image = ?) |] - [ ":user_id" := userId, - ":contact_id" := contactId, - ":display_name" := displayName, - ":full_name" := fullName, - ":image" := image - ] + (userId, contactId, displayName, fullName, image, image) rights <$> mapM (runExceptT . getContact db userId) contactIds createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> Contact -> ExceptT StoreError IO (Probe, Int64) @@ -1127,7 +1141,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, custom_user_profile_id, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at FROM connections WHERE user_id = ? AND agent_conn_id = ? @@ -1139,15 +1153,15 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.created_at, c.updated_at + SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.created_at, c.updated_at FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt)] = - let profile = Profile {displayName, fullName, image} + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt)] = + let profile = LocalProfile {profileId, displayName, fullName, image} in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) @@ -1161,18 +1175,18 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? |] (groupMemberId, userId, userContactId) @@ -1249,20 +1263,20 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc @@ -1284,30 +1298,32 @@ updateConnectionStatus db Connection {connId} connStatus = do DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo -createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do +createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo +createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do let GroupProfile {displayName, fullName, image} = groupProfile currentTs <- getCurrentTime - withLocalDisplayName db userId displayName $ \ldn -> do - DB.execute - db - "INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (displayName, fullName, image, userId, currentTs, currentTs) - profileId <- insertedRowId db - DB.execute - db - "INSERT INTO groups (local_display_name, user_id, group_profile_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (ldn, userId, profileId, currentTs, currentTs) - groupId <- insertedRowId db - memberId <- encodedRandomBytes gVar 12 - membership <- createContactMember_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser currentTs + withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do + groupId <- liftIO $ do + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (displayName, fullName, image, userId, currentTs, currentTs) + profileId <- insertedRowId db + DB.execute + db + "INSERT INTO groups (local_display_name, user_id, group_profile_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (ldn, userId, profileId, currentTs, currentTs) + insertedRowId db + memberId <- liftIO $ encodedRandomBytes gVar 12 + -- TODO ldn from incognito profile + membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> ExceptT StoreError IO GroupInfo -createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} = +createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo +createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, fromMemberProfile, invitedMember, connRequest, groupProfile} incognitoProfile = do liftIO getInvitationGroupId_ >>= \case - Nothing -> ExceptT createGroupInvitation_ + Nothing -> createGroupInvitation_ -- TODO treat the case that the invitation details could've changed Just gId -> getGroupInfo db user gId where @@ -1315,24 +1331,81 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInv getInvitationGroupId_ = maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId) - createGroupInvitation_ :: IO (Either StoreError GroupInfo) + createGroupInvitation_ :: ExceptT StoreError IO GroupInfo createGroupInvitation_ = do let GroupProfile {displayName, fullName, image} = groupProfile - withLocalDisplayName db userId displayName $ \localDisplayName -> do - currentTs <- getCurrentTime + ExceptT $ + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + currentTs <- liftIO getCurrentTime + groupId <- liftIO $ do + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (displayName, fullName, image, userId, currentTs, currentTs) + profileId <- insertedRowId db + DB.execute + db + "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (profileId, localDisplayName, connRequest, userId, currentTs, currentTs) + insertedRowId db + _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs + membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs + pure GroupInfo {groupId, localDisplayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + +createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember +createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do + customUserProfileId <- liftIO $ createIncognitoProfile_ db userId createdAt incognitoProfile + (localDisplayName, memberProfile) <- case (incognitoProfile, customUserProfileId) of + (Just profile@Profile {displayName}, Just profileId) -> + (,toLocalProfile profileId profile) <$> insertMemberIncognitoProfile_ displayName profileId + _ -> (,profile' userOrContact) <$> liftIO insertMember_ + groupMemberId <- liftIO $ insertedRowId db + pure + GroupMember + { groupMemberId, + groupId, + memberId, + memberRole, + memberCategory, + memberStatus, + invitedBy, + localDisplayName, + memberProfile, + memberContactId = Just $ contactId' userOrContact, + memberContactProfileId = localProfileId (profile' userOrContact), + activeConn = Nothing + } + where + insertMember_ :: IO ContactName + insertMember_ = do + let localDisplayName = localDisplayName' userOrContact + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, memberId, memberRole, memberCategory, memberStatus, fromInvitedBy userContactId invitedBy) + :. (userId, localDisplayName' userOrContact, contactId' userOrContact, localProfileId $ profile' userOrContact, createdAt, createdAt) + ) + pure localDisplayName + insertMemberIncognitoProfile_ :: ContactName -> ProfileId -> ExceptT StoreError IO ContactName + insertMemberIncognitoProfile_ incognitoDisplayName customUserProfileId = ExceptT $ + withLocalDisplayName db userId incognitoDisplayName $ \incognitoLdn -> do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (displayName, fullName, image, userId, currentTs, currentTs) - profileId <- insertedRowId db - DB.execute - db - "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (profileId, localDisplayName, connRequest, userId, currentTs, currentTs) - groupId <- insertedRowId db - _ <- createContactMember_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown currentTs - membership <- createContactMember_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) currentTs - pure $ GroupInfo {groupId, localDisplayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, memberId, memberRole, memberCategory, memberStatus, fromInvitedBy userContactId invitedBy) + :. (userId, incognitoLdn, contactId' userOrContact, localProfileId $ profile' userOrContact, customUserProfileId, createdAt, createdAt) + ) + pure $ Right incognitoLdn setGroupInvitationChatItemId :: DB.Connection -> User -> GroupId -> ChatItemId -> IO () setGroupInvitationChatItemId db User {userId} groupId chatItemId = do @@ -1390,11 +1463,11 @@ getUserGroupDetails db User {userId, userContactId} = [sql| SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, mp.display_name, mp.full_name, mp.image + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, mp.contact_profile_id, mp.display_name, mp.full_name, mp.image FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members m USING (group_id) - JOIN contact_profiles mp USING (contact_profile_id) + JOIN contact_profiles mp ON mp.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) WHERE g.user_id = ? AND m.contact_id = ? |] (userId, userContactId) @@ -1419,11 +1492,11 @@ getGroupMember db user@User {userId} groupId groupMemberId = [sql| SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc @@ -1441,11 +1514,11 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do [sql| SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc @@ -1479,29 +1552,61 @@ getGroupInvitation db user groupId = do findFromContact (IBContact contactId) = find ((== Just contactId) . memberContactId) findFromContact _ = const Nothing -type GroupMemberRow = (Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Maybe Int64, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData) +type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData)) -type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Int64, Maybe ContactName, Maybe Int64, Maybe ContactName, Maybe Text, Maybe ImageData) +type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData)) toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId (groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, localDisplayName, memberContactId, displayName, fullName, image) = - let memberProfile = Profile {displayName, fullName, image} +toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image)) = + let memberProfile = LocalProfile {profileId, displayName, fullName, image} invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing in GroupMember {..} toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId (Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus, invitedById, Just localDisplayName, memberContactId, Just displayName, Just fullName, image) = - Just $ toGroupMember userContactId (groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, localDisplayName, memberContactId, displayName, fullName, image) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image)) toMaybeGroupMember _ _ = Nothing -createContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> Int64 -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember -createContactMember db gVar user groupId contact memberRole agentConnId connRequest = +createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember +createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole agentConnId connRequest = createWithRandomId gVar $ \memId -> do - currentTs <- getCurrentTime - member@GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId contact (MemberIdRole (MemberId memId) memberRole) GCInviteeMember GSMemInvited IBUser (Just connRequest) currentTs - void $ createMemberConnection_ db (userId user) groupMemberId agentConnId Nothing 0 currentTs + createdAt <- liftIO getCurrentTime + member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt + void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt pure member + where + createMember_ memberId createdAt = do + insertMember_ + groupMemberId <- liftIO $ insertedRowId db + pure + GroupMember + { groupMemberId, + groupId, + memberId, + memberRole, + memberCategory = GCInviteeMember, + memberStatus = GSMemInvited, + invitedBy = IBUser, + localDisplayName, + memberProfile = profile, + memberContactId = Just contactId, + memberContactProfileId = localProfileId profile, + activeConn = Nothing + } + where + insertMember_ = + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, sent_inv_queue_info, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, memberId, memberRole, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser) + :. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt) + ) getMemberInvitation :: DB.Connection -> User -> Int64 -> IO (Maybe ConnReqInvitation) getMemberInvitation db User {userId} groupMemberId = @@ -1516,18 +1621,33 @@ createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () updateGroupMemberStatus db userId GroupMember {groupMemberId} memStatus = do currentTs <- getCurrentTime - DB.executeNamed + DB.execute db [sql| UPDATE group_members - SET member_status = :member_status, updated_at = :updated_at - WHERE user_id = :user_id AND group_member_id = :group_member_id + SET member_status = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? |] - [ ":user_id" := userId, - ":group_member_id" := groupMemberId, - ":member_status" := memStatus, - ":updated_at" := currentTs - ] + (memStatus, currentTs, userId, groupMemberId) + +createMemberIncognitoProfile :: DB.Connection -> UserId -> GroupMember -> Maybe Profile -> ExceptT StoreError IO GroupMember +createMemberIncognitoProfile db userId m@GroupMember {groupMemberId} incognitoProfile = do + currentTs <- liftIO getCurrentTime + customUserProfileId <- liftIO $ createIncognitoProfile_ db userId currentTs incognitoProfile + case (incognitoProfile, customUserProfileId) of + (Just profile@Profile {displayName}, Just profileId) -> + ExceptT $ + withLocalDisplayName db userId displayName $ \incognitoLdn -> do + DB.execute + db + [sql| + UPDATE group_members + SET local_display_name = ?, member_profile_id = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + (incognitoLdn, profileId, currentTs, userId, groupMemberId) + pure . Right $ m {localDisplayName = incognitoLdn, memberProfile = toLocalProfile profileId profile} + _ -> pure m -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember @@ -1549,7 +1669,7 @@ createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile memContactId = Nothing, memProfileId } - createNewMember_ db user gInfo newMember currentTs + Right <$> createNewMember_ db user gInfo newMember currentTs createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember createNewMember_ @@ -1563,7 +1683,7 @@ createNewMember_ memInvitedBy = invitedBy, localDisplayName, memContactId = memberContactId, - memProfileId + memProfileId = memberContactProfileId } createdAt = do let invitedById = fromInvitedBy userContactId invitedBy @@ -1573,12 +1693,12 @@ createNewMember_ [sql| INSERT INTO group_members (group_id, member_id, member_role, member_category, member_status, - invited_by, user_id, local_display_name, contact_profile_id, contact_id, created_at, updated_at) + invited_by, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - (groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memProfileId, memberContactId, createdAt, createdAt) + (groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) groupMemberId <- insertedRowId db - pure GroupMember {..} + pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile, memberContactId, memberContactProfileId, activeConn} deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId} = do @@ -1684,11 +1804,23 @@ getIntroduction_ db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> ExceptT StoreError IO GroupMember -createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId = do +getGroupMemberProfileId :: DB.Connection -> UserId -> GroupMember -> ExceptT StoreError IO Int64 +getGroupMemberProfileId db userId GroupMember {groupMemberId, groupId} = + ExceptT . firstRow fromOnly (SEGroupMemberNotFound {groupId, groupMemberId}) $ + DB.query + db + [sql| + SELECT contact_profile_id + FROM group_members + WHERE user_id = ? AND group_member_id = ? + |] + (userId, groupMemberId) + +createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> Maybe ProfileId -> ExceptT StoreError IO GroupMember +createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId customUserProfileId = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- liftIO getCurrentTime - Connection {connId = directConnId} <- liftIO $ createMemberContactConnection_ db userId directAgentConnId memberContactId cLevel currentTs + Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId memberContactId Nothing customUserProfileId cLevel currentTs (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile (Just groupId) currentTs liftIO $ do let newMember = @@ -1705,12 +1837,12 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM conn <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId memberContactId cLevel currentTs pure (member :: GroupMember) {activeConn = Just conn} -createIntroToMemberContact :: DB.Connection -> UserId -> GroupMember -> GroupMember -> ConnId -> ConnId -> IO () -createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} groupAgentConnId directAgentConnId = do +createIntroToMemberContact :: DB.Connection -> UserId -> GroupMember -> GroupMember -> ConnId -> ConnId -> Maybe ProfileId -> IO () +createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} groupAgentConnId directAgentConnId customUserProfileId = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- getCurrentTime void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs - Connection {connId = directConnId} <- createMemberContactConnection_ db userId directAgentConnId viaContactId cLevel currentTs + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId viaContactId Nothing customUserProfileId cLevel currentTs contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs where @@ -1740,48 +1872,7 @@ createIntroToMemberContact db userId GroupMember {memberContactId = viaContactId [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing - -createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> UTCTime -> IO GroupMember -createContactMember_ db user groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy = - createContactMemberInv_ db user groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy Nothing - -createContactMemberInv_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ConnReqInvitation -> UTCTime -> IO GroupMember -createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy connRequest createdAt = do - insertMember_ - groupMemberId <- insertedRowId db - let memberProfile = profile' userOrContact - memberContactId = Just $ contactId' userOrContact - localDisplayName = localDisplayName' userOrContact - activeConn = Nothing - pure GroupMember {..} - where - insertMember_ = - DB.executeNamed - db - [sql| - INSERT INTO group_members - ( group_id, member_id, member_role, member_category, member_status, invited_by, - user_id, local_display_name, contact_profile_id, contact_id, sent_inv_queue_info, created_at, updated_at) - VALUES - (:group_id,:member_id,:member_role,:member_category,:member_status,:invited_by, - :user_id,:local_display_name, - (SELECT contact_profile_id FROM contacts WHERE contact_id = :contact_id), - :contact_id, :sent_inv_queue_info, :created_at, :updated_at) - |] - [ ":group_id" := groupId, - ":member_id" := memberId, - ":member_role" := memberRole, - ":member_category" := memberCategory, - ":member_status" := memberStatus, - ":invited_by" := fromInvitedBy userContactId invitedBy, - ":user_id" := userId, - ":local_display_name" := localDisplayName' userOrContact, - ":contact_id" := contactId' userOrContact, - ":sent_inv_queue_info" := connRequest, - ":created_at" := createdAt, - ":updated_at" := createdAt - ] +createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing Nothing getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember db User {userId, userContactId} Contact {contactId} = @@ -1794,21 +1885,21 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, p.image, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id - JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc @@ -1831,8 +1922,8 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = db [sql| SELECT - ct.contact_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id @@ -1847,9 +1938,9 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: (Int64, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow -> Contact - toContact' ((contactId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow) = - let profile = Profile {displayName, fullName, image} + toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow -> Contact + toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image} activeConn = toConnection connRow in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} @@ -1957,7 +2048,7 @@ getChatRefByFileId db User {userId} fileId = createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection createSndFileConnection_ db userId fileId agentConnId = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing 0 currentTs + createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing Nothing 0 currentTs updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -1975,14 +2066,12 @@ createSndFileChunk db SndFileTransfer {fileId, connId, fileSize, chunkSize} = do pure $ case map fromOnly ns of [] -> Just 1 n : _ -> if n * chunkSize >= fileSize then Nothing else Just (n + 1) - insertChunk = \case - Just chunkNo -> do - currentTs <- getCurrentTime - DB.execute - db - "INSERT OR REPLACE INTO snd_file_chunks (file_id, connection_id, chunk_number, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, connId, chunkNo, currentTs, currentTs) - Nothing -> pure () + insertChunk chunkNo_ = forM_ chunkNo_ $ \chunkNo -> do + currentTs <- getCurrentTime + DB.execute + db + "INSERT OR REPLACE INTO snd_file_chunks (file_id, connection_id, chunk_number, created_at, updated_at) VALUES (?,?,?,?,?)" + (fileId, connId, chunkNo, currentTs, currentTs) updateSndFileChunkMsg :: DB.Connection -> SndFileTransfer -> Integer -> AgentMsgId -> IO () updateSndFileChunkMsg db SndFileTransfer {fileId, connId} chunkNo msgId = do @@ -2491,9 +2580,7 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q |] ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) ciId <- insertedRowId db - case msgId_ of - Just msgId -> insertChatItemMessage_ db ciId msgId createdAt - Nothing -> pure () + forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, CIStatus d, Maybe SharedMsgId, UTCTime, UTCTime) @@ -2555,10 +2642,10 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, - m.member_status, m.invited_by, m.local_display_name, m.contact_id, + m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image FROM group_members m - JOIN contact_profiles p ON m.contact_profile_id = p.contact_profile_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id LEFT JOIN chat_items i ON i.group_id = m.group_id AND m.group_member_id = i.group_member_id @@ -2593,9 +2680,9 @@ getDirectChatPreviews_ db User {userId} = do [sql| SELECT -- Contact - ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), @@ -2661,7 +2748,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), @@ -2671,18 +2758,18 @@ getGroupChatPreviews_ db User {userId, userContactId} = do f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, -- Maybe GroupMember - sender m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, - m.member_status, m.invited_by, m.local_display_name, m.contact_id, + m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, - rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, + rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN ( SELECT group_id, MAX(chat_item_id) AS MaxId FROM chat_items @@ -2699,10 +2786,10 @@ getGroupChatPreviews_ db User {userId, userContactId} = do GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id - LEFT JOIN contact_profiles rp ON rp.contact_profile_id = rm.contact_profile_id + LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id) WHERE g.user_id = ? AND mu.contact_id = ? ORDER BY i.item_ts DESC |] @@ -2744,13 +2831,13 @@ getContactConnectionChatPreviews_ db User {userId} _ = <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL |] (userId, ConnContact) where - toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, UTCTime, UTCTime) -> AChat + toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, UTCTime, UTCTime) -> AChat toContactConnectionChatPreview connRow = let conn = toPendingContactConnection connRow stats = ChatStats {unreadCount = 0, minUnreadItemId = 0} @@ -2762,7 +2849,7 @@ getPendingContactConnection db userId connId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, created_at, updated_at FROM connections WHERE user_id = ? AND connection_id = ? @@ -2788,9 +2875,9 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, UTCTime, UTCTime) -> PendingContactConnection -toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, createdAt, updatedAt) = - PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, createdAt, updatedAt} +toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, UTCTime, UTCTime) -> PendingContactConnection +toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt) = + PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt} getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChat db user contactId pagination search_ = do @@ -2925,9 +3012,9 @@ getContact db userId contactId = [sql| SELECT -- Contact - ct.contact_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.conn_status, c.conn_type, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -3053,12 +3140,12 @@ getGroupInfo db User {userId, userContactId} groupId = g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, - mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, + mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = mu.contact_profile_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? |] (groupId, userId, userContactId) @@ -3073,7 +3160,7 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou currentTs <- getCurrentTime updateGroupProfile_ currentTs updateGroup_ ldn currentTs - pure $ (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p'} + pure . Right $ (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p'} where updateGroupProfile_ currentTs = DB.execute @@ -3422,21 +3509,21 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, - m.member_status, m.invited_by, m.local_display_name, m.contact_id, + m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, - rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, + rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id - LEFT JOIN contact_profiles rp ON rp.contact_profile_id = rm.contact_profile_id + LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id) WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id = ? |] (userId, groupId, itemId) @@ -3748,7 +3835,7 @@ getCalls db User {userId} = do -- | Saves unique local display name based on passed displayName, suffixed with _N if required. -- This function should be called inside transaction. -withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO a) -> IO (Either StoreError a) +withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO (Either StoreError a)) -> IO (Either StoreError a) withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreateName` 20) where getLdnSuffix :: IO Int @@ -3769,7 +3856,7 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate currentTs <- getCurrentTime let ldn = displayName <> (if ldnSuffix == 0 then "" else T.pack $ '_' : show ldnSuffix) E.try (insertName ldn currentTs) >>= \case - Right () -> Right <$> action ldn + Right () -> action ldn Left e | DB.sqlError e == DB.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1) | otherwise -> E.throwIO e @@ -3843,6 +3930,7 @@ data StoreError | SEChatItemSharedMsgIdNotFound {sharedMsgId :: SharedMsgId} | SEChatItemNotFoundByFileId {fileId :: FileTransferId} | SEChatItemNotFoundByGroupId {groupId :: GroupId} + | SEProfileNotFound {profileId :: Int64} deriving (Show, Exception, Generic) instance ToJSON StoreError where diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 80d99490f0..95d01d2a20 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -22,6 +22,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Int (Int64) +import Data.Maybe (isJust) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime) @@ -39,7 +40,7 @@ import Simplex.Messaging.Util ((<$?>)) class IsContact a where contactId' :: a -> ContactId - profile' :: a -> Profile + profile' :: a -> LocalProfile localDisplayName' :: a -> ContactName instance IsContact User where @@ -56,7 +57,7 @@ data User = User { userId :: UserId, userContactId :: ContactId, localDisplayName :: ContactName, - profile :: Profile, + profile :: LocalProfile, activeUser :: Bool } deriving (Show, Generic, FromJSON) @@ -67,10 +68,12 @@ type UserId = ContactId type ContactId = Int64 +type ProfileId = Int64 + data Contact = Contact { contactId :: ContactId, localDisplayName :: ContactName, - profile :: Profile, + profile :: LocalProfile, activeConn :: Connection, viaGroup :: Maybe Int64, createdAt :: UTCTime, @@ -88,6 +91,9 @@ contactConn = activeConn contactConnId :: Contact -> ConnId contactConnId Contact {activeConn} = aConnId activeConn +contactConnIncognito :: Contact -> Bool +contactConnIncognito Contact {activeConn = Connection {customUserProfileId}} = isJust customUserProfileId + data ContactRef = ContactRef { contactId :: ContactId, localDisplayName :: ContactName @@ -198,6 +204,7 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, image :: Maybe ImageData + -- incognito field should not be read as is into this data type to prevent sending it as part of profile to contacts } deriving (Eq, Show, Generic, FromJSON) @@ -205,6 +212,29 @@ instance ToJSON Profile where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +data LocalProfile = LocalProfile + { profileId :: ProfileId, + displayName :: ContactName, + fullName :: Text, + image :: Maybe ImageData + } + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON LocalProfile where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + +localProfileId :: LocalProfile -> ProfileId +localProfileId = profileId + +toLocalProfile :: ProfileId -> Profile -> LocalProfile +toLocalProfile profileId Profile {displayName, fullName, image} = + LocalProfile {profileId, displayName, fullName, image} + +fromLocalProfile :: LocalProfile -> Profile +fromLocalProfile LocalProfile {displayName, fullName, image} = + Profile {displayName, fullName, image} + data GroupProfile = GroupProfile { displayName :: GroupName, fullName :: Text, @@ -232,13 +262,16 @@ instance FromField ImageData where fromField = fmap ImageData . fromField data GroupInvitation = GroupInvitation { fromMember :: MemberIdRole, + fromMemberProfile :: Maybe Profile, invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON GroupInvitation where toEncoding = J.genericToEncoding J.defaultOptions +instance ToJSON GroupInvitation where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} data MemberIdRole = MemberIdRole { memberId :: MemberId, @@ -267,7 +300,7 @@ instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptio memberInfo :: GroupMember -> MemberInfo memberInfo GroupMember {memberId, memberRole, memberProfile} = - MemberInfo memberId memberRole memberProfile + MemberInfo memberId memberRole (fromLocalProfile memberProfile) data ReceivedGroupInvitation = ReceivedGroupInvitation { fromMember :: GroupMember, @@ -278,6 +311,8 @@ data ReceivedGroupInvitation = ReceivedGroupInvitation type GroupMemberId = Int64 +-- memberProfile's profileId is COALESCE(member_profile_id, contact_profile_id), member_profile_id is non null +-- if incognito profile was saved for member (used for hosts and invitees in incognito groups) data GroupMember = GroupMember { groupMemberId :: GroupMemberId, groupId :: GroupId, @@ -287,8 +322,9 @@ data GroupMember = GroupMember memberStatus :: GroupMemberStatus, invitedBy :: InvitedBy, localDisplayName :: ContactName, - memberProfile :: Profile, - memberContactId :: Maybe Int64, + memberProfile :: LocalProfile, + memberContactId :: Maybe ContactId, + memberContactProfileId :: ProfileId, activeConn :: Maybe Connection } deriving (Eq, Show, Generic) @@ -306,6 +342,9 @@ memberConnId GroupMember {activeConn} = aConnId <$> activeConn groupMemberId' :: GroupMember -> GroupMemberId groupMemberId' GroupMember {groupMemberId} = groupMemberId +memberIncognito :: GroupMember -> Bool +memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId + data NewGroupMember = NewGroupMember { memInfo :: MemberInfo, memCategory :: GroupMemberCategory, @@ -695,6 +734,7 @@ data Connection = Connection connLevel :: Int, viaContact :: Maybe Int64, -- group member contact ID, if not direct connection viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" + customUserProfileId :: Maybe Int64, connType :: ConnType, connStatus :: ConnStatus, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID @@ -705,6 +745,9 @@ data Connection = Connection aConnId :: Connection -> ConnId aConnId Connection {agentConnId = AgentConnId cId} = cId +connCustomUserProfileId :: Connection -> Maybe Int64 +connCustomUserProfileId Connection {customUserProfileId} = customUserProfileId + instance ToJSON Connection where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} @@ -715,6 +758,7 @@ data PendingContactConnection = PendingContactConnection pccConnStatus :: ConnStatus, viaContactUri :: Bool, viaUserContactLink :: Maybe Int64, + customUserProfileId :: Maybe Int64, createdAt :: UTCTime, updatedAt :: UTCTime } diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 426f22b66e..443ce285fe 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -54,7 +54,7 @@ serializeChatResponse = unlines . map unStyle . responseToView False responseToView :: Bool -> ChatResponse -> [StyledString] responseToView testView = \case - CRActiveUser User {profile} -> viewUserProfile profile + CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] @@ -64,8 +64,8 @@ responseToView testView = \case CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] CRUserSMPServers smpServers -> viewSMPServers smpServers testView CRNetworkConfig cfg -> viewNetworkConfig cfg - CRContactInfo ct cStats -> viewContactInfo ct cStats - CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats + CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile + CRGroupMemberInfo g m cStats mainProfile -> viewGroupMemberInfo g m cStats mainProfile CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item False CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems CRChatItemStatusUpdated _ -> [] @@ -89,10 +89,10 @@ responseToView testView = \case CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] - CRGroupCreated g -> viewGroupCreated g + CRGroupCreated g customUserProfile -> viewGroupCreated g customUserProfile testView CRGroupMembers g -> viewGroupMembers g CRGroupsList gs -> viewGroupsList gs - CRSentGroupInvitation g c _ -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + CRSentGroupInvitation g c _ sentCustomProfile -> viewSentGroupInvitation g c sentCustomProfile CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus CRUserProfile p -> viewUserProfile p CRUserProfileNoChange -> ["user profile did not change"] @@ -100,7 +100,7 @@ responseToView testView = \case CRChatCmdError e -> viewChatError e CRInvitation cReq -> viewConnReqInvitation cReq CRSentConfirmation -> ["confirmation sent!"] - CRSentInvitation -> ["connection request sent!"] + CRSentInvitation customUserProfile -> viewSentInvitation customUserProfile testView CRContactDeleted c -> [ttyContact' c <> ": contact is deleted"] CRChatCleared chatInfo -> viewChatCleared chatInfo CRAcceptingContactRequest c -> [ttyFullContact c <> ": accepting contact request..."] @@ -129,7 +129,7 @@ responseToView testView = \case CRSndFileRcvCancelled _ ft@SndFileTransfer {recipientDisplayName = c} -> [ttyContact c <> " cancelled receiving " <> sndFile ft] CRContactConnecting _ -> [] - CRContactConnected ct -> [ttyFullContact ct <> ": contact is connected"] + CRContactConnected ct userCustomProfile -> viewContactConnected ct userCustomProfile testView CRContactAnotherClient c -> [ttyContact' c <> ": contact is connected to another client"] CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] @@ -138,13 +138,13 @@ responseToView testView = \case [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" where (errors, subscribed) = partition (isJust . contactError) summary + CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} -> + [groupInvitation' ldn fullName $ memberIncognito membership] + CRReceivedGroupInvitation g c role receivedCustomProfile -> viewReceivedGroupInvitation g c role receivedCustomProfile + CRUserJoinedGroup g _ usedCustomProfile -> viewUserJoinedGroup g usedCustomProfile testView + CRJoinedGroupMember g m mainProfile -> viewJoinedGroupMember g m mainProfile CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] - CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} -> - [groupInvitation' ldn fullName] - CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role - CRUserJoinedGroup g _ -> [ttyGroup' g <> ": you joined the group"] - CRJoinedGroupMember g m -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] CRJoinedGroupMemberConnecting g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] CRConnectedToGroupMember g m -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] CRDeletedMemberUser g by -> [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g @@ -156,7 +156,7 @@ responseToView testView = \case CRGroupUpdated g g' m -> viewGroupUpdated g g' m CRMemberSubError g m e -> [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e] CRMemberSubSummary summary -> viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" - CRGroupSubscribed g -> [ttyFullGroup g <> ": connected to server(s)"] + CRGroupSubscribed g -> viewGroupSubscribed g CRPendingSubSummary _ -> [] CRSndFileSubError SndFileTransfer {fileId, fileName} e -> ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] @@ -208,6 +208,12 @@ responseToView testView = \case contactList :: [ContactRef] -> String contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs +viewGroupSubscribed :: GroupInfo -> [StyledString] +viewGroupSubscribed g@GroupInfo {membership} = + [incognito <> ttyFullGroup g <> ": connected to server(s)"] + where + incognito = if memberIncognito membership then incognitoPrefix else "" + showSMPServer :: SMPServer -> String showSMPServer = B.unpack . strEncode . host @@ -363,6 +369,12 @@ viewConnReqInvitation cReq = "and ask them to connect: " <> highlight' "/c " ] +viewSentGroupInvitation :: GroupInfo -> Contact -> Maybe Profile -> [StyledString] +viewSentGroupInvitation g c sentCustomProfile = + if isJust sentCustomProfile + then ["invitation to join the group " <> ttyGroup' g <> " incognito sent to " <> ttyContact' c] + else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] @@ -372,7 +384,8 @@ viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of viewContactsList :: [Contact] -> [StyledString] viewContactsList = let ldn = T.toLower . (localDisplayName :: Contact -> ContactName) - in map ttyFullContact . sortOn ldn + incognito ct = if contactConnIncognito ct then incognitoPrefix else "" + in map (\ct -> incognito ct <> ttyFullContact ct) . sortOn ldn viewUserContactLinkDeleted :: [StyledString] viewUserContactLinkDeleted = @@ -396,6 +409,17 @@ autoAcceptStatus_ autoAccept autoReply = ("auto_accept " <> if autoAccept then "on" else "off") : maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply +viewSentInvitation :: Maybe Profile -> Bool -> [StyledString] +viewSentInvitation incognitoProfile testView = + case incognitoProfile of + Just profile -> + if testView + then incognitoProfile' profile : message + else message + where + message = ["connection request sent incognito!"] + Nothing -> ["connection request sent!"] + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", @@ -403,11 +427,22 @@ viewReceivedContactRequest c Profile {fullName} = "to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)" ] -viewGroupCreated :: GroupInfo -> [StyledString] -viewGroupCreated g@GroupInfo {localDisplayName} = - [ "group " <> ttyFullGroup g <> " is created", - "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" - ] +viewGroupCreated :: GroupInfo -> Maybe Profile -> Bool -> [StyledString] +viewGroupCreated g@GroupInfo {localDisplayName} incognitoProfile testView = + case incognitoProfile of + Just profile -> + if testView + then incognitoProfile' profile : message + else message + where + message = + [ "group " <> ttyFullGroup g <> " is created incognito, your profile for this group: " <> incognitoProfile' profile, + "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" + ] + Nothing -> + [ "group " <> ttyFullGroup g <> " is created", + "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" + ] viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = @@ -415,11 +450,33 @@ viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = "to re-send invitation: " <> highlight ("/rm " <> gn <> " " <> c) <> ", " <> highlight ("/a " <> gn <> " " <> c) ] -viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] -viewReceivedGroupInvitation g c role = - [ ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role), - "use " <> highlight ("/j " <> groupName' g) <> " to accept" - ] +viewUserJoinedGroup :: GroupInfo -> Bool -> Bool -> [StyledString] +viewUserJoinedGroup g@GroupInfo {membership = GroupMember {memberProfile}} incognito testView = + if incognito + then + if testView + then incognitoProfile' (fromLocalProfile memberProfile) : incognitoMessage + else incognitoMessage + else [ttyGroup' g <> ": you joined the group"] + where + incognitoMessage = [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] + +viewJoinedGroupMember :: GroupInfo -> GroupMember -> Maybe Profile -> [StyledString] +viewJoinedGroupMember g m@GroupMember {localDisplayName} = \case + Just Profile {displayName = mainProfileName} -> [ttyGroup' g <> ": " <> ttyContact mainProfileName <> " joined the group incognito as " <> styleIncognito localDisplayName] + Nothing -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] + +viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> Maybe Profile -> [StyledString] +viewReceivedGroupInvitation g c role hostIncognitoProfile = + case hostIncognitoProfile of + Just profile -> + [ ttyFullGroup g <> ": " <> ttyContact' c <> " (known to the group as " <> incognitoProfile' profile <> ") invites you to join the group incognito as " <> plain (strEncode role), + "use " <> highlight ("/j " <> groupName' g) <> " to join this group incognito" + ] + Nothing -> + [ ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role), + "use " <> highlight ("/j " <> groupName' g) <> " to accept" + ] groupPreserved :: GroupInfo -> [StyledString] groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"] @@ -434,7 +491,8 @@ viewGroupMembers :: Group -> [StyledString] viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members where removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft - groupMember m = ttyFullMember m <> ": " <> role m <> ", " <> category m <> status m + groupMember m = incognito m <> ttyFullMember m <> ": " <> role m <> ", " <> category m <> status m + incognito m = if memberIncognito m then incognitoPrefix else "" role m = plain . strEncode $ memberRole (m :: GroupMember) category m = case memberCategory m of GCUserMember -> "you, " @@ -450,6 +508,21 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt GSMemCreator -> "created group" _ -> "" +viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString] +viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView = + case userIncognitoProfile of + Just profile -> + if testView + then incognitoProfile' profile : message + else message + where + message = + [ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile, + "use " <> highlight ("/info " <> localDisplayName) <> " to print out this incognito profile again" + ] + Nothing -> + [ttyFullContact ct <> ": contact is connected"] + viewGroupsList :: [GroupInfo] -> [StyledString] viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g "] viewGroupsList gs = map groupSS $ sortOn ldn_ gs @@ -457,9 +530,10 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName) groupSS GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} = case memberStatus membership of - GSMemInvited -> groupInvitation' ldn fullName - s -> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s + GSMemInvited -> groupInvitation' ldn fullName $ memberIncognito membership + s -> incognito <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s where + incognito = if memberIncognito membership then incognitoPrefix else "" viewMemberStatus = \case GSMemRemoved -> delete "you are removed" GSMemLeft -> delete "you left" @@ -467,15 +541,20 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs _ -> "" delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" -groupInvitation' :: GroupName -> Text -> StyledString -groupInvitation' displayName fullName = +groupInvitation' :: GroupName -> Text -> Bool -> StyledString +groupInvitation' displayName fullName membershipIncognito = highlight ("#" <> displayName) <> optFullName displayName fullName - <> " - you are invited (" + <> invitationText <> highlight ("/j " <> displayName) <> " to join, " <> highlight ("/d #" <> displayName) <> " to delete invitation)" + where + invitationText = + if membershipIncognito + then " - you are invited incognito (" + else " - you are invited (" viewContactsMerged :: Contact -> Contact -> [StyledString] viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} = @@ -514,16 +593,24 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = "use `/network socks=[ timeout=]` to change settings" ] -viewContactInfo :: Contact -> ConnectionStats -> [StyledString] -viewContactInfo Contact {contactId} stats = +viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] +viewContactInfo Contact {contactId} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewConnectionStats stats + <> maybe + ["you've shared main profile with this contact"] + (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) + incognitoProfile -viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId} stats = +viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] +viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId} stats mainProfile = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats + <> maybe + ["unknown whether group member uses his main profile or incognito one for the group"] + (\Profile {displayName, fullName} -> ["member is using " <> styleIncognito' "incognito" <> " profile for the group, main profile known: " <> ttyFullName displayName fullName]) + mainProfile viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvServers, sndServers} = @@ -559,8 +646,8 @@ viewGroupUpdated viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated - Contact {localDisplayName = n, profile = Profile {fullName}} - Contact {localDisplayName = n', profile = Profile {fullName = fullName'}} + Contact {localDisplayName = n, profile = LocalProfile {fullName}} + Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName'}} | n == n' && fullName == fullName' = [] | n == n' = ["contact " <> ttyContact n <> fullNameUpdate] | otherwise = @@ -817,6 +904,7 @@ viewChatError = \case CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"] CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"] CEGroupUserRole -> ["you have insufficient permissions for this group command"] + CEGroupNotIncognitoCantInvite -> ["you're using main profile for this group - prohibited to invite contact to whom you are connected incognito"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> groupName' g)] CEGroupMemberNotActive -> ["you cannot invite other members yet, try later"] @@ -883,14 +971,14 @@ ttyContact' :: Contact -> StyledString ttyContact' Contact {localDisplayName = c} = ttyContact c ttyFullContact :: Contact -> StyledString -ttyFullContact Contact {localDisplayName, profile = Profile {fullName}} = +ttyFullContact Contact {localDisplayName, profile = LocalProfile {fullName}} = ttyFullName localDisplayName fullName ttyMember :: GroupMember -> StyledString ttyMember GroupMember {localDisplayName} = ttyContact localDisplayName ttyFullMember :: GroupMember -> StyledString -ttyFullMember GroupMember {localDisplayName, memberProfile = Profile {fullName}} = +ttyFullMember GroupMember {localDisplayName, memberProfile = LocalProfile {fullName}} = ttyFullName localDisplayName fullName ttyFullName :: ContactName -> Text -> StyledString @@ -909,7 +997,8 @@ ttyFromContactDeleted :: ContactName -> StyledString ttyFromContactDeleted c = ttyFrom $ c <> "> [deleted] " ttyToContact' :: Contact -> StyledString -ttyToContact' Contact {localDisplayName = c} = ttyToContact c +ttyToContact' Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} = + maybe "" (const incognitoPrefix) customUserProfileId <> ttyToContact c ttyQuotedContact :: Contact -> StyledString ttyQuotedContact Contact {localDisplayName = c} = ttyFrom $ c <> ">" @@ -919,7 +1008,8 @@ ttyQuotedMember (Just GroupMember {localDisplayName = c}) = "> " <> ttyFrom c ttyQuotedMember _ = "> " <> ttyFrom "?" ttyFromContact' :: Contact -> StyledString -ttyFromContact' Contact {localDisplayName = c} = ttyFromContact c +ttyFromContact' Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} = + maybe "" (const incognitoPrefix) customUserProfileId <> ttyFromContact c ttyGroup :: GroupName -> StyledString ttyGroup g = styled (colored Blue) $ "#" <> g @@ -949,10 +1039,12 @@ ttyFrom :: Text -> StyledString ttyFrom = styled $ colored Yellow ttyFromGroup' :: GroupInfo -> GroupMember -> StyledString -ttyFromGroup' g GroupMember {localDisplayName = m} = ttyFromGroup g m +ttyFromGroup' g@GroupInfo {membership} GroupMember {localDisplayName = m} = + (if memberIncognito membership then incognitoPrefix else "") <> ttyFromGroup g m ttyToGroup :: GroupInfo -> StyledString -ttyToGroup GroupInfo {localDisplayName = g} = styled (colored Cyan) $ "#" <> g <> " " +ttyToGroup GroupInfo {localDisplayName = g, membership} = + (if memberIncognito membership then incognitoPrefix else "") <> styled (colored Cyan) ("#" <> g <> " ") ttyFilePath :: FilePath -> StyledString ttyFilePath = plain @@ -960,12 +1052,24 @@ ttyFilePath = plain optFullName :: ContactName -> Text -> StyledString optFullName localDisplayName fullName = plain $ optionalFullName localDisplayName fullName +incognitoPrefix :: StyledString +incognitoPrefix = styleIncognito' "i " + +incognitoProfile' :: Profile -> StyledString +incognitoProfile' Profile {displayName} = styleIncognito displayName + highlight :: StyledFormat a => a -> StyledString highlight = styled $ colored Cyan highlight' :: String -> StyledString highlight' = highlight +styleIncognito :: StyledFormat a => a -> StyledString +styleIncognito = styled $ colored Magenta + +styleIncognito' :: String -> StyledString +styleIncognito' = styleIncognito + styleTime :: String -> StyledString styleTime = Styled [SetColor Foreground Vivid Black] diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index c44a49bfca..4d60e6538d 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -19,7 +19,7 @@ import qualified Data.Text as T import Simplex.Chat.Call import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Options (ChatOpts (..)) -import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..)) +import Simplex.Chat.Types (ConnStatus (..), ImageData (..), LocalProfile (..), Profile (..), User (..)) import Simplex.Messaging.Util (unlessM) import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.FilePath (()) @@ -88,6 +88,13 @@ chatTests = do it "reject contact and delete contact link" testRejectContactAndDeleteUserContact it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "auto-reply message" testAutoReplyMessage + describe "incognito mode" $ do + it "connect incognito via invitation link" testConnectIncognitoInvitationLink + it "connect incognito via contact address" testConnectIncognitoContactAddress + it "accept contact request incognito" testAcceptContactRequestIncognito + it "create group incognito" testCreateGroupIncognito + it "join group incognito" testJoinGroupIncognito + it "can't invite contact to whom user connected incognito to non incognito group" testCantInviteIncognitoConnectionNonIncognitoGroup describe "SMP servers" $ it "get and set SMP servers" testGetSetSMPServers describe "async connection handshake" $ do @@ -2045,6 +2052,394 @@ testAutoReplyMessage = testChat2 aliceProfile bobProfile $ alice <# "@bob hello!" ] +testConnectIncognitoInvitationLink :: IO () +testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + alice #$> ("/incognito on", id, "ok") + bob #$> ("/incognito on", id, "ok") + alice ##> "/c" + inv <- getInvitation alice + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + bobIncognito <- getTermLine bob + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ do + bob <## (aliceIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## ("use /info " <> aliceIncognito <> " to print out this incognito profile again"), + do + alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("use /info " <> bobIncognito <> " to print out this incognito profile again") + ] + -- after turning incognito mode off conversation is incognito + alice #$> ("/incognito off", id, "ok") + bob #$> ("/incognito off", id, "ok") + alice ?#> ("@" <> bobIncognito <> " psst, I'm incognito") + bob ?<# (aliceIncognito <> "> psst, I'm incognito") + bob ?#> ("@" <> aliceIncognito <> " me too") + alice ?<# (bobIncognito <> "> me too") + -- new contact is connected non incognito + connectUsers alice cath + alice <##> cath + -- bob is not notified on profile change + alice ##> "/p alice" + concurrentlyN_ + [ alice <## "user full name removed (your contacts are notified)", + cath <## "contact alice removed full name" + ] + alice ?#> ("@" <> bobIncognito <> " do you see that I've changed profile?") + bob ?<# (aliceIncognito <> "> do you see that I've changed profile?") + bob ?#> ("@" <> aliceIncognito <> " no") + alice ?<# (bobIncognito <> "> no") + +testConnectIncognitoContactAddress :: IO () +testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob #$> ("/incognito on", id, "ok") + bob ##> ("/c " <> cLink) + bobIncognito <- getTermLine bob + bob <## "connection request sent incognito!" + alice <## (bobIncognito <> " wants to connect to you!") + alice <## ("to accept: /ac " <> bobIncognito) + alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)") + alice ##> ("/ac " <> bobIncognito) + alice <## (bobIncognito <> ": accepting contact request...") + _ <- getTermLine bob + concurrentlyN_ + [ do + bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## "use /info alice to print out this incognito profile again", + alice <## (bobIncognito <> ": contact is connected") + ] + -- after turning incognito mode off conversation is incognito + alice #$> ("/incognito off", id, "ok") + bob #$> ("/incognito off", id, "ok") + alice #> ("@" <> bobIncognito <> " who are you?") + bob ?<# "alice> who are you?" + bob ?#> "@alice I'm Batman" + alice <# (bobIncognito <> "> I'm Batman") + +testAcceptContactRequestIncognito :: IO () +testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob ##> ("/c " <> cLink) + alice <#? bob + alice #$> ("/incognito on", id, "ok") + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## "use /info bob to print out this incognito profile again" + ] + -- after turning incognito mode off conversation is incognito + alice #$> ("/incognito off", id, "ok") + bob #$> ("/incognito off", id, "ok") + alice ?#> "@bob my profile is totally inconspicuous" + bob <# (aliceIncognito <> "> my profile is totally inconspicuous") + bob #> ("@" <> aliceIncognito <> " I know!") + alice ?<# "bob> I know!" + +testCreateGroupIncognito :: IO () +testCreateGroupIncognito = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + -- non incognito connections + connectUsers alice cath + connectUsers bob cath + -- bob connected incognito to alice + alice ##> "/c" + inv <- getInvitation alice + bob #$> ("/incognito on", id, "ok") + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + bobIncognito <- getTermLine bob + concurrentlyN_ + [ do + bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## "use /info alice to print out this incognito profile again", + alice <## (bobIncognito <> ": contact is connected") + ] + -- alice creates group incognito + alice #$> ("/incognito on", id, "ok") + alice ##> "/g secret_club" + aliceMemIncognito <- getTermLine alice + alice <## ("group #secret_club is created incognito, your profile for this group: " <> aliceMemIncognito) + alice <## "use /a secret_club to add members" + alice ##> ("/a secret_club " <> bobIncognito) + concurrentlyN_ + [ alice <## ("invitation to join the group #secret_club incognito sent to " <> bobIncognito), + do + bob <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin") + bob <## "use /j secret_club to join this group incognito" + ] + -- bob uses different profile when joining group + bob ##> "/j secret_club" + bobMemIncognito <- getTermLine bob + concurrently_ + (alice <## ("#secret_club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito)) + (bob <## ("#secret_club: you joined the group incognito as " <> bobMemIncognito)) + -- cath is invited incognito + alice ##> "/a secret_club cath" + concurrentlyN_ + [ alice <## "invitation to join the group #secret_club incognito sent to cath", + do + cath <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin") + cath <## "use /j secret_club to join this group incognito" + ] + cath ##> "/j secret_club" + cathMemIncognito <- getTermLine cath + -- bob and cath don't merge contacts + concurrentlyN_ + [ alice <## ("#secret_club: cath joined the group incognito as " <> cathMemIncognito), + do + cath <## ("#secret_club: you joined the group incognito as " <> cathMemIncognito) + cath <## ("#secret_club: member " <> bobMemIncognito <> " is connected"), + do + bob <## ("#secret_club: " <> aliceMemIncognito <> " added " <> cathMemIncognito <> " to the group (connecting...)") + bob <## ("#secret_club: new member " <> cathMemIncognito <> " is connected") + ] + -- send messages - group is incognito for everybody + alice #$> ("/incognito off", id, "ok") + bob #$> ("/incognito off", id, "ok") + cath #$> ("/incognito off", id, "ok") + alice ?#> "#secret_club hello" + concurrently_ + (bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) + (cath ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) + bob ?#> "#secret_club hi there" + concurrently_ + (alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi there")) + (cath ?<# ("#secret_club " <> bobMemIncognito <> "> hi there")) + cath ?#> "#secret_club hey" + concurrently_ + (alice ?<# ("#secret_club " <> cathMemIncognito <> "> hey")) + (bob ?<# ("#secret_club " <> cathMemIncognito <> "> hey")) + -- bob and cath can send messages via direct incognito connections + bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob") + cath ?<# (bobMemIncognito <> "> hi, I'm bob") + cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath") + bob ?<# (cathMemIncognito <> "> hey, I'm cath") + -- non incognito connections are separate + bob <##> cath + -- list groups + alice ##> "/gs" + alice <## "i #secret_club" + -- list group members + alice ##> "/ms secret_club" + alice + <### [ "i " <> aliceMemIncognito <> ": owner, you, created group", + "i " <> bobMemIncognito <> ": admin, invited, connected", + "i " <> cathMemIncognito <> ": admin, invited, connected" + ] + -- remove member + bob ##> ("/rm secret_club " <> cathMemIncognito) + concurrentlyN_ + [ bob <## ("#secret_club: you removed " <> cathMemIncognito <> " from the group"), + alice <## ("#secret_club: " <> bobMemIncognito <> " removed " <> cathMemIncognito <> " from the group"), + do + cath <## ("#secret_club: " <> bobMemIncognito <> " removed you from the group") + cath <## "use /d #secret_club to delete the group" + ] + bob ?#> "#secret_club hi" + concurrently_ + (alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi")) + (cath "#secret_club hello" + concurrently_ + (bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) + (cath "#secret_club hello" + cath <## "you are no longer a member of the group" + bob ?#> ("@" <> cathMemIncognito <> " I removed you from group") + cath ?<# (bobMemIncognito <> "> I removed you from group") + cath ?#> ("@" <> bobMemIncognito <> " ok") + bob ?<# (cathMemIncognito <> "> ok") + +testJoinGroupIncognito :: IO () +testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + -- non incognito connections + connectUsers alice cath + connectUsers bob cath + connectUsers cath dan + -- bob connected incognito to alice + alice ##> "/c" + inv <- getInvitation alice + bob #$> ("/incognito on", id, "ok") + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + bobIncognito <- getTermLine bob + concurrentlyN_ + [ do + bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## "use /info alice to print out this incognito profile again", + alice <## (bobIncognito <> ": contact is connected") + ] + -- alice creates group non incognito + alice ##> "/g club" + alice <## "group #club is created" + alice <## "use /a club to add members" + alice ##> ("/a club " <> bobIncognito) + concurrentlyN_ + [ alice <## ("invitation to join the group #club sent to " <> bobIncognito), + do + bob <## "#club: alice invites you to join the group as admin" + bob <## "use /j club to accept" + ] + -- since bob is connected incognito to host, he uses different profile when joining group even though he turned incognito mode off + bob #$> ("/incognito off", id, "ok") + bob ##> "/j club" + bobMemIncognito <- getTermLine bob + concurrently_ + (alice <## ("#club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito)) + (bob <## ("#club: you joined the group incognito as " <> bobMemIncognito)) + -- cath joins incognito + alice ##> "/a club cath" + concurrentlyN_ + [ alice <## "invitation to join the group #club sent to cath", + do + cath <## "#club: alice invites you to join the group as admin" + cath <## "use /j club to accept" + ] + cath #$> ("/incognito on", id, "ok") + cath ##> "/j club" + cathMemIncognito <- getTermLine cath + -- bob and cath don't merge contacts + concurrentlyN_ + [ alice <## ("#club: cath joined the group incognito as " <> cathMemIncognito), + do + cath <## ("#club: you joined the group incognito as " <> cathMemIncognito) + cath <## ("#club: member " <> bobMemIncognito <> " is connected"), + do + bob <## ("#club: alice added " <> cathMemIncognito <> " to the group (connecting...)") + bob <## ("#club: new member " <> cathMemIncognito <> " is connected") + ] + -- cath invites dan incognito + cath ##> "/a club dan" + concurrentlyN_ + [ cath <## "invitation to join the group #club incognito sent to dan", + do + dan <## ("#club: cath (known to the group as " <> cathMemIncognito <> ") invites you to join the group incognito as admin") + dan <## "use /j club to join this group incognito" + ] + dan ##> "/j club" + danMemIncognito <- getTermLine dan + concurrentlyN_ + [ cath <## ("#club: dan joined the group incognito as " <> danMemIncognito), + do + dan <## ("#club: you joined the group incognito as " <> danMemIncognito) + dan + <### [ "#club: member alice (Alice) is connected", + "#club: member " <> bobMemIncognito <> " is connected" + ], + do + alice <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)") + alice <## ("#club: new member " <> danMemIncognito <> " is connected"), + do + bob <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)") + bob <## ("#club: new member " <> danMemIncognito <> " is connected") + ] + -- send messages - group is incognito for cath and dan + alice #$> ("/incognito off", id, "ok") + bob #$> ("/incognito off", id, "ok") + cath #$> ("/incognito off", id, "ok") + dan #$> ("/incognito off", id, "ok") + alice #> "#club hello" + concurrentlyN_ + [ bob ?<# "#club alice> hello", + cath ?<# "#club alice> hello", + dan ?<# "#club alice> hello" + ] + bob ?#> "#club hi there" + concurrentlyN_ + [ alice <# ("#club " <> bobMemIncognito <> "> hi there"), + cath ?<# ("#club " <> bobMemIncognito <> "> hi there"), + dan ?<# ("#club " <> bobMemIncognito <> "> hi there") + ] + cath ?#> "#club hey" + concurrentlyN_ + [ alice <# ("#club " <> cathMemIncognito <> "> hey"), + bob ?<# ("#club " <> cathMemIncognito <> "> hey"), + dan ?<# ("#club " <> cathMemIncognito <> "> hey") + ] + dan ?#> "#club how is it going?" + concurrentlyN_ + [ alice <# ("#club " <> danMemIncognito <> "> how is it going?"), + bob ?<# ("#club " <> danMemIncognito <> "> how is it going?"), + cath ?<# ("#club " <> danMemIncognito <> "> how is it going?") + ] + -- bob and cath can send messages via direct incognito connections + bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob") + cath ?<# (bobMemIncognito <> "> hi, I'm bob") + cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath") + bob ?<# (cathMemIncognito <> "> hey, I'm cath") + -- non incognito connections are separate + bob <##> cath + -- bob and dan can send messages via direct incognito connections + bob ?#> ("@" <> danMemIncognito <> " hi, I'm bob") + dan ?<# (bobMemIncognito <> "> hi, I'm bob") + dan ?#> ("@" <> bobMemIncognito <> " hey, I'm dan") + bob ?<# (danMemIncognito <> "> hey, I'm dan") + -- list group members + alice ##> "/ms club" + alice + <### [ "alice (Alice): owner, you, created group", + "i " <> bobMemIncognito <> ": admin, invited, connected", + "i " <> cathMemIncognito <> ": admin, invited, connected", + danMemIncognito <> ": admin, connected" + ] + bob ##> "/ms club" + bob + <### [ "alice (Alice): owner, host, connected", + "i " <> bobMemIncognito <> ": admin, you, connected", + cathMemIncognito <> ": admin, connected", + danMemIncognito <> ": admin, connected" + ] + cath ##> "/ms club" + cath + <### [ "alice (Alice): owner, host, connected", + bobMemIncognito <> ": admin, connected", + "i " <> cathMemIncognito <> ": admin, you, connected", + "i " <> danMemIncognito <> ": admin, invited, connected" + ] + dan ##> "/ms club" + dan + <### [ "alice (Alice): owner, connected", + bobMemIncognito <> ": admin, connected", + "i " <> cathMemIncognito <> ": admin, host, connected", + "i " <> danMemIncognito <> ": admin, you, connected" + ] + +testCantInviteIncognitoConnectionNonIncognitoGroup :: IO () +testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- alice connected incognito to bob + alice #$> ("/incognito on", id, "ok") + alice ##> "/c" + inv <- getInvitation alice + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## "use /info bob to print out this incognito profile again" + ] + -- alice creates group non incognito + alice #$> ("/incognito off", id, "ok") + alice ##> "/g club" + alice <## "group #club is created" + alice <## "use /a club to add members" + alice ##> "/a club bob" + alice <## "you're using main profile for this group - prohibited to invite contact to whom you are connected incognito" + testGetSetSMPServers :: IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ @@ -2513,7 +2908,7 @@ connectUsers cc1 cc2 = do showName :: TestCC -> IO String showName (TestCC ChatController {currentUser} _ _ _ _) = do - Just User {localDisplayName, profile = Profile {fullName}} <- readTVarIO currentUser + Just User {localDisplayName, profile = LocalProfile {fullName}} <- readTVarIO currentUser pure . T.unpack $ localDisplayName <> " (" <> fullName <> ")" createGroup2 :: String -> TestCC -> TestCC -> IO () @@ -2580,6 +2975,11 @@ cc #> cmd = do cc `send` cmd cc <# cmd +(?#>) :: TestCC -> String -> IO () +cc ?#> cmd = do + cc `send` cmd + cc <# ("i " <> cmd) + (#$>) :: (Eq a, Show a) => TestCC -> (String, String -> a, a) -> Expectation cc #$> (cmd, f, res) = do cc ##> cmd @@ -2632,6 +3032,9 @@ getInAnyOrder f cc ls = do (<#) :: TestCC -> String -> Expectation cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line +(?<#) :: TestCC -> String -> Expectation +cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line + ( Expectation ( Date: Thu, 18 Aug 2022 08:43:35 +0100 Subject: [PATCH 26/89] update simplexmq (servers update) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 7384a7ae18..b8beece48c 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: b2d4bfd0a8f7471c2cbd67f195391436bffcdf63 + tag: 3fc9afbb351033413dd84344ec603910e370a8ec source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b0040f9ffe..7d06411d5c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."b2d4bfd0a8f7471c2cbd67f195391436bffcdf63" = "0j4hn96gc6c6kxrg5fgmcgg1865wc3b54ndr496csd7vz9iyfviv"; + "https://github.com/simplex-chat/simplexmq.git"."3fc9afbb351033413dd84344ec603910e370a8ec" = "1zkjlrqffyv90wxsxjx73zmksrv79bvg7hklzmp1iac5pxqvdjfv"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 5047d02691..f199a4225f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: b2d4bfd0a8f7471c2cbd67f195391436bffcdf63 + commit: 3fc9afbb351033413dd84344ec603910e370a8ec # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From b15d39eb4aedbbb4e3eb6f668947e0d57759c7de Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 18 Aug 2022 09:18:15 +0100 Subject: [PATCH 27/89] android: include default values in JSON encoding (#943) --- .../app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index ada4d57020..138d452083 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1346,6 +1346,7 @@ data class KeepAliveOpts( val json = Json { prettyPrint = true ignoreUnknownKeys = true + encodeDefaults = true } @Serializable From d8049d4bfcd777660902fb93332a636f9246f862 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 18 Aug 2022 14:14:22 +0300 Subject: [PATCH 28/89] Fixes service issues (#942) * Fixes service issues - no more crashes after start of a service with battery optimization enabled - no more service restarts after the app exit with disabled private notifications * Disable service restart even after reboot if the user didn't allow this * [Experimental] Disabling logic of start up process from application process. The same is enabled in a service anyway - every application process creation makes the service running even in situations when it's not needed. For example, RescheduleReceiver from WorkManager receives boot completed event and it triggers service start up * Returned unneeded part of code. It may be useful (in theory) if user's device doesn't allow any services to be started (it's something that should never happen) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/MainActivity.kt | 20 +---------- .../main/java/chat/simplex/app/SimplexApp.kt | 33 +++++++++++++++++-- .../java/chat/simplex/app/SimplexService.kt | 22 ++++++++++++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 9 +++-- .../app/views/usersettings/SettingsView.kt | 3 +- 5 files changed, 62 insertions(+), 25 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 5d71b4f17b..163d09a8af 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -70,7 +70,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { } } } - schedulePeriodicServiceRestartWorker() + SimplexApp.context.schedulePeriodicServiceRestartWorker() } override fun onNewIntent(intent: Intent?) { @@ -131,24 +131,6 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { } } - private fun schedulePeriodicServiceRestartWorker() { - val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get() - val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) { - Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy") - ExistingPeriodicWorkPolicy.KEEP - } else { - Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy") - chatController.appPrefs.autoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION) - ExistingPeriodicWorkPolicy.REPLACE - } - val work = PeriodicWorkRequestBuilder(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES) - .addTag(SimplexService.TAG) - .addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) - .build() - Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes") - WorkManager.getInstance(this)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work) - } - private fun setPerformLA(on: Boolean) { vm.chatModel.controller.appPrefs.laNoticeShown.set(true) if (on) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 7d0e1620ec..39a4806add 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -4,14 +4,17 @@ import android.app.Application import android.net.LocalServerSocket import android.util.Log import androidx.lifecycle.* +import androidx.work.* import chat.simplex.app.model.* import chat.simplex.app.views.helpers.getFilesDirectory import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.onboarding.OnboardingStage +import kotlinx.coroutines.* import java.io.BufferedReader import java.io.InputStreamReader import java.util.* import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit import kotlin.concurrent.thread const val TAG = "SIMPLEX" @@ -57,8 +60,6 @@ class SimplexApp: Application(), LifecycleEventObserver { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo } else { chatController.startChat(user) - SimplexService.start(applicationContext) - chatController.showBackgroundServiceNoticeIfNeeded() } } } @@ -80,6 +81,34 @@ class SimplexApp: Application(), LifecycleEventObserver { } } + fun allowToStartServiceAfterAppExit() = with(chatModel.controller) { + appPrefs.runServiceInBackground.get() && isIgnoringBatteryOptimizations(chatModel.controller.appContext) + } + + /* + * It takes 1-10 milliseconds to process this function. Better to do it in a background thread + * */ + fun schedulePeriodicServiceRestartWorker() = CoroutineScope(Dispatchers.Default).launch { + if (!allowToStartServiceAfterAppExit()) { + return@launch + } + val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get() + val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) { + Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy") + ExistingPeriodicWorkPolicy.KEEP + } else { + Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy") + chatController.appPrefs.autoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION) + ExistingPeriodicWorkPolicy.REPLACE + } + val work = PeriodicWorkRequestBuilder(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES) + .addTag(SimplexService.TAG) + .addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) + .build() + Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes") + WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work) + } + companion object { lateinit var context: SimplexApp private set diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index a9426a062e..134c1591ef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -2,6 +2,7 @@ package chat.simplex.app import android.app.* import android.content.* +import android.content.pm.PackageManager import android.os.* import android.provider.Settings import android.util.Log @@ -54,7 +55,10 @@ class SimplexService: Service() { override fun onDestroy() { Log.d(TAG, "Simplex service destroyed") stopService() - sendBroadcast(Intent(this, AutoRestartReceiver::class.java)) // Restart if necessary! + + // If private notifications are enabled and battery optimization is disabled, restart the service + if (SimplexApp.context.allowToStartServiceAfterAppExit()) + sendBroadcast(Intent(this, AutoRestartReceiver::class.java)) super.onDestroy() } @@ -147,6 +151,11 @@ class SimplexService: Service() { // re-schedules the task when "Clear recent apps" is pressed override fun onTaskRemoved(rootIntent: Intent) { + // If private notifications aren't enabled or battery optimization isn't disabled, we shouldn't restart the service + if (!SimplexApp.context.allowToStartServiceAfterAppExit()) { + return + } + val restartServiceIntent = Intent(applicationContext, SimplexService::class.java).also { it.setPackage(packageName) }; @@ -162,6 +171,17 @@ class SimplexService: Service() { Log.d(TAG, "StartReceiver: onReceive called") scheduleStart(context) } + companion object { + fun toggleReceiver(enable: Boolean) { + Log.d(TAG, "StartReceiver: toggleReceiver enabled: $enable") + val component = ComponentName(BuildConfig.APPLICATION_ID, StartReceiver::class.java.name) + SimplexApp.context.packageManager.setComponentEnabledSetting( + component, + if (enable) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } } // restart on destruction diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 138d452083..7700bd8ef0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -5,6 +5,7 @@ import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application import android.content.* +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.PowerManager @@ -890,7 +891,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun showBackgroundServiceNoticeIfNeeded() { Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") if (!appPrefs.backgroundServiceNoticeShown.get()) { - // the branch for the new users who has never seen service notice + // the branch for the new users who have never seen service notice if (isIgnoringBatteryOptimizations(appContext)) { showBGServiceNotice() } else { @@ -903,15 +904,19 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager // the branch for users who have app installed, and have seen the service notice, // but the battery optimization for the app is on (Android 12) AND the service is running if (appPrefs.backgroundServiceBatteryNoticeShown.get()) { - // users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service + // users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service showDisablingServiceNotice() appPrefs.runServiceInBackground.set(false) chatModel.runServiceInBackground.value = false + SimplexService.StartReceiver.toggleReceiver(false) } else { // show battery optimization notice showBGServiceNoticeIgnoreOptimization() appPrefs.backgroundServiceBatteryNoticeShown.set(true) } + } else { + // service is allowed and battery optimization is disabled + SimplexApp.context.schedulePeriodicServiceRestartWorker() } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 74d8970a20..8d5358c52b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* -import chat.simplex.app.BuildConfig +import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* @@ -44,6 +44,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { } chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.runServiceInBackground.value = on + SimplexService.StartReceiver.toggleReceiver(on) } if (user != null) { From 3221c0abb5869b3194ef9a2219397378b50ec240 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:17:18 +0400 Subject: [PATCH 29/89] docs: optional profile in groupInvitation and x.grp.acpt (incognito connections) (#944) --- docs/protocol/simplex-chat.schema.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index 379969bb59..76612d22fb 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -82,7 +82,7 @@ "memberId": {"ref": "base64url"}, "metadata": { "comment": "memberId must be present in all group message references, both for sent and received" - } + } } }, "fileInvitation": { @@ -108,6 +108,12 @@ "invitedMember": {"ref": "memberIdRole"}, "connRequest": {"ref": "connReqUri"}, "groupProfile": {"ref": "profile"} + }, + "optionalProperties": { + "fromMemberProfile": {"ref": "profile"}, + "metadata": { + "comment": "fromMemberProfile is user's custom profile to be used in the group - invitee should use this profile for the host's group member" + } } }, "memberIdRole": { @@ -156,7 +162,7 @@ "rtcExtraInfo": { "properties": { "rtcIceCandidates": {"type": "string"} - } + } } } }, @@ -210,7 +216,7 @@ "profile": {"ref": "profile"} } } - } + } }, "x.info.probe": { "properties": { @@ -317,6 +323,12 @@ "params": { "properties": { "memberId": {"ref": "base64url"} + }, + "optionalProperties": { + "memberProfile": {"ref": "profile"}, + "metadata": { + "comment": "memberProfile is user's custom profile to be used in the group - host should use this profile for the invitee's group member" + } } } } From 70168967a31fdddb0cfd71394a593f2c2a883d8c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:17:05 +0100 Subject: [PATCH 30/89] core: commands to set chat notification settings (#946) * core: commands to set chat notification settings * add API --- .../java/chat/simplex/app/model/ChatModel.kt | 2 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 8 ++ apps/ios/SimpleXChat/APITypes.swift | 9 ++ apps/ios/SimpleXChat/ChatTypes.swift | 2 + cabal.project | 2 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 51 ++++++---- src/Simplex/Chat/Controller.hs | 1 + .../M20220818_chat_notifications.hs | 14 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 4 +- src/Simplex/Chat/Store.hs | 93 +++++++++++-------- src/Simplex/Chat/Types.hs | 13 +++ stack.yaml | 2 +- 13 files changed, 146 insertions(+), 56 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 291299bc15..cc4f7046ef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -460,6 +460,7 @@ class Contact( val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, +// val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -540,6 +541,7 @@ class GroupInfo ( override val localDisplayName: String, val groupProfile: GroupProfile, val membership: GroupMember, + // val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 7700bd8ef0..fe5008e4db 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1136,6 +1136,7 @@ sealed class CC { class SetUserSMPServers(val smpServers: List): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() class APIGetNetworkConfig: CC() + class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC() class APIContactInfo(val contactId: Long): CC() class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC() class AddContact: CC() @@ -1186,6 +1187,7 @@ sealed class CC { is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" + is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" is AddContact -> "/connect" @@ -1237,6 +1239,7 @@ sealed class CC { is SetUserSMPServers -> "setUserSMPServers" is APISetNetworkConfig -> "/apiSetNetworkConfig" is APIGetNetworkConfig -> "/apiGetNetworkConfig" + is APISetChatSettings -> "/apiSetChatSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" is AddContact -> "addContact" @@ -1348,6 +1351,11 @@ data class KeepAliveOpts( } } +@Serializable +data class ChatSettings( + val enableNtfs: Boolean +) + val json = Json { prettyPrint = true ignoreUnknownKeys = true diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b0bc469594..64ee7b6970 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -45,6 +45,7 @@ public enum ChatCommand { case setUserSMPServers(smpServers: [String]) case apiSetNetworkConfig(networkConfig: NetCfg) case apiGetNetworkConfig + case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) case addContact @@ -108,6 +109,7 @@ public enum ChatCommand { case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" + case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" case .addContact: return "/connect" @@ -170,6 +172,7 @@ public enum ChatCommand { case .setUserSMPServers: return "setUserSMPServers" case .apiSetNetworkConfig: return "apiSetNetworkConfig" case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetChatSettings: return "apiSetChatSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" case .addContact: return "addContact" @@ -591,6 +594,12 @@ public struct KeepAliveOpts: Codable, Equatable { public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) } +public struct ChatSettings: Codable { + public var enableNtfs: Bool + + public static let defaults: ChatSettings = ChatSettings(enableNtfs: true) +} + public struct ConnectionStats: Codable { public var rcvServers: [String]? public var sndServers: [String]? diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index d51e8f911d..fd82605e65 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -244,6 +244,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var profile: Profile public var activeConn: Connection public var viaGroup: Int64? +// public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date @@ -433,6 +434,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { var localDisplayName: GroupName public var groupProfile: GroupProfile public var membership: GroupMember +// public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date diff --git a/cabal.project b/cabal.project index b8beece48c..e73877f169 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 3fc9afbb351033413dd84344ec603910e370a8ec + tag: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd source-repository-package type: git diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 33be0c784c..cdd4cdf838 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -44,6 +44,7 @@ library Simplex.Chat.Migrations.M20220715_groups_chat_item_id Simplex.Chat.Migrations.M20220811_chat_items_indices Simplex.Chat.Migrations.M20220812_incognito_profiles + Simplex.Chat.Migrations.M20220818_chat_notifications Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index eba26284ec..a28f43f61f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -261,7 +261,7 @@ processChatCommand = \case setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) setupSndFileTransfer ct = forM file_ $ \file -> do (fileSize, chSize) <- checkSndFile file - (agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, fileConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize @@ -597,6 +597,23 @@ processChatCommand = \case pure CRCmdOk APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk APIGetNetworkConfig -> CRNetworkConfig <$> withUser' (\_ -> withAgent getNetworkConfig) + APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user@User {userId} -> case cType of + CTDirect -> do + ct <- withStore $ \db -> do + ct <- getContact db userId chatId + liftIO $ updateContactSettings db user chatId chatSettings + pure ct + withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (enableNtfs chatSettings) + pure CRCmdOk + CTGroup -> do + ms <- withStore $ \db -> do + Group _ ms <- getGroup db user chatId + liftIO $ updateGroupSettings db user chatId chatSettings + pure ms + withAgent $ \a -> forM_ ms $ \m -> + forM_ (memberConnId m) $ \connId -> toggleConnectionNtfs a connId (enableNtfs chatSettings) + pure CRCmdOk + _ -> pure $ chatCmdError "not supported" APIContactInfo contactId -> withUser $ \User {userId} -> do -- [incognito] print user's incognito profile for this contact ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db userId contactId @@ -621,7 +638,7 @@ processChatCommand = \case -- [incognito] generate profile for connection incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - (connId, cReq) <- withAgent (`createConnection` SCMInvitation) + (connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation conn <- withStore' $ \db -> createDirectConnection db userId connId ConnNew incognitoProfile toView $ CRNewContactConnection conn pure $ CRInvitation cReq @@ -630,7 +647,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile - connId <- withAgent $ \a -> joinConnection a cReq . directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> joinConnection a True cReq . directMessage $ XInfo profileToSend conn <- withStore' $ \db -> createDirectConnection db userId connId ConnJoined incognitoProfile toView $ CRNewContactConnection conn pure CRSentConfirmation @@ -649,7 +666,7 @@ processChatCommand = \case processChatCommand $ APIClearChat (ChatRef CTDirect contactId) ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do - (connId, cReq) <- withAgent (`createConnection` SCMContact) + (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact withStore $ \db -> createUserContactLink db userId connId cReq pure $ CRUserContactLinkCreated cReq DeleteMyAddress -> withUser $ \user -> withChatLock $ do @@ -731,7 +748,7 @@ processChatCommand = \case case contactMember contact members of Nothing -> do gVar <- asks idsDrg - (agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation member cReq Just member@GroupMember {groupMemberId, memberStatus} @@ -760,7 +777,7 @@ processChatCommand = \case else pure g -- [incognito] if membership is incognito, send its incognito profile in XGrpAcpt let incognitoProfile = if memberIncognito membership' then Just (fromLocalProfile $ memberProfile membership') else Nothing - agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile + agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId updateGroupMemberStatus db userId fromMember GSMemAccepted @@ -959,7 +976,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe profile incognitoProfile - connId <- withAgent $ \a -> joinConnection a cReq $ directMessage (XContact profileToSend $ Just xContactId) + connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId) conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile toView $ CRNewContactConnection conn pure $ CRSentInvitation incognitoProfile @@ -1108,7 +1125,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F case fileConnReq of -- direct file protocol Just connReq -> - tryError (withAgent $ \a -> joinConnection a connReq . directMessage $ XFileAcpt fName) >>= \case + tryError (withAgent $ \a -> joinConnection a True connReq . directMessage $ XFileAcpt fName) >>= \case Right agentConnId -> do filePath <- getRcvFilePath filePath_ fName withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnJoined filePath @@ -1122,7 +1139,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F case activeConn of Just conn -> do sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - (agentConnId, fileInvConnReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation filePath <- getRcvFilePath filePath_ fName ci <- withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath void $ sendDirectMessage conn (XFileAcptInv sharedMsgId fileInvConnReq fName) (GroupId groupId) @@ -1173,7 +1190,7 @@ acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationI incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile - connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId incognitoProfile agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () @@ -1455,7 +1472,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> pure () processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m () - processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership} m@GroupMember {memberContactProfileId} = case agentMsg of + processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m@GroupMember {memberContactProfileId} = case agentMsg of CONF confId _ connInfo -> do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo case memberCategory m of @@ -1497,6 +1514,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unless (memberActive membership) $ updateGroupMemberStatus db userId membership GSMemConnected sendPendingGroupMessages m conn + unless (enableNtfs chatSettings) . withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) False case memberCategory m of GCHostMember -> do -- [incognito] chat item & event with indication that host connected incognito @@ -1886,7 +1904,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unless cancelled $ if fName == fileName then - tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XOk) >>= \case + tryError (withAgent $ \a -> joinConnection a True fileConnReq . directMessage $ XOk) >>= \case Right acId -> withStore' $ \db -> createSndGroupFileTransferConnection db userId fileId acId m Left e -> throwError e @@ -2091,8 +2109,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage if isMember memId gInfo members then messageWarning "x.grp.mem.intro ignored: member already exists" else do - (groupConnId, groupConnReq) <- withAgent (`createConnection` SCMInvitation) - (directConnId, directConnReq) <- withAgent (`createConnection` SCMInvitation) + (groupConnId, groupConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation + (directConnId, directConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation -- [incognito] direct connection with member has to be established using same incognito profile customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId customUserProfileId @@ -2126,8 +2144,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage withStore' $ \db -> saveMemberInvitation db toMember introInv -- [incognito] send membership incognito profile, create direct connection as incognito let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) - groupConnId <- withAgent $ \a -> joinConnection a groupConnReq $ directMessage msg - directConnId <- withAgent $ \a -> joinConnection a directConnReq $ directMessage msg + groupConnId <- withAgent $ \a -> joinConnection a True groupConnReq $ directMessage msg + directConnId <- withAgent $ \a -> joinConnection a True directConnReq $ directMessage msg customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId customUserProfileId @@ -2579,6 +2597,7 @@ chatCommandP = "/_network " *> (APISetNetworkConfig <$> jsonP), ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, + "/_settings" *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), "/_info @" *> (APIContactInfo <$> A.decimal), ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 173bd90382..416fa97b53 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -150,6 +150,7 @@ data ChatCommand | SetUserSMPServers [SMPServer] | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | APISetChatSettings ChatRef ChatSettings | APIContactInfo ContactId | APIGroupMemberInfo GroupId GroupMemberId | ContactInfo ContactName diff --git a/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs b/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs new file mode 100644 index 0000000000..ffb2b15967 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220818_chat_notifications where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220818_chat_notifications :: Query +m20220818_chat_notifications = + [sql| +ALTER TABLE contacts ADD COLUMN enable_ntfs INTEGER; + +ALTER TABLE groups ADD COLUMN enable_ntfs INTEGER; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ce4c6553fd..f4f7ac3697 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -54,6 +54,7 @@ is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, + enable_ntfs INTEGER, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -119,7 +120,8 @@ CREATE TABLE groups( inv_queue_info BLOB, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), - chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, -- received + chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, + enable_ntfs INTEGER, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 456bb21634..74ca466072 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -176,6 +176,8 @@ module Simplex.Chat.Store getCalls, getPendingContactConnection, deletePendingContactConnection, + updateContactSettings, + updateGroupSettings, withTransaction, ) where @@ -227,6 +229,7 @@ import Simplex.Chat.Migrations.M20220702_calls import Simplex.Chat.Migrations.M20220715_groups_chat_item_id import Simplex.Chat.Migrations.M20220811_chat_items_indices import Simplex.Chat.Migrations.M20220812_incognito_profiles +import Simplex.Chat.Migrations.M20220818_chat_notifications import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -257,7 +260,8 @@ schemaMigrations = ("20220702_calls", m20220702_calls), ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), ("20220811_chat_items_indices", m20220811_chat_items_indices), - ("20220812_incognito_profiles", m20220812_incognito_profiles) + ("20220812_incognito_profiles", m20220812_incognito_profiles), + ("20220818_chat_notifications", m20220818_chat_notifications) ] -- | The list of migrations in ascending order by date @@ -362,7 +366,7 @@ getConnReqContactXContactId db userId cReqHash = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -445,7 +449,7 @@ createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> Excep createDirectContact db userId activeConn@Connection {connId} profile = do createdAt <- liftIO getCurrentTime (localDisplayName, contactId, profileId) <- createContact_ db userId connId profile Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt} createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs = @@ -566,20 +570,22 @@ updateContact_ db userId contactId displayName newName updatedAt = do (newName, updatedAt, userId, contactId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId) -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, UTCTime, UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) toContact :: ContactRow :. ConnectionRow -> Contact -toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = +toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} activeConn = toConnection connRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = +toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in case toMaybeConnection connRow of Just activeConn -> - Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} _ -> Left $ SEContactNotReady localDisplayName -- TODO return the last connection that is ready, not any last connection @@ -759,7 +765,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -878,11 +884,11 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile u customUserProfileId <- createIncognitoProfile_ db userId createdAt incognitoProfile DB.execute db - "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" - (userId, localDisplayName, profileId, createdAt, createdAt, xContactId) + "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?,?)" + (userId, localDisplayName, profileId, True, createdAt, createdAt, xContactId) contactId <- insertedRowId db activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt = createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -1153,16 +1159,17 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.created_at, c.updated_at + SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.enable_ntfs, c.created_at, c.updated_at FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt)] = + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt)] = let profile = LocalProfile {profileId, displayName, fullName, image} - in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -1172,7 +1179,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1260,7 +1267,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1311,13 +1318,14 @@ createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = Except profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (local_display_name, user_id, group_profile_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (ldn, userId, profileId, currentTs, currentTs) + "INSERT INTO groups (local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (ldn, userId, profileId, True, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 -- TODO ldn from incognito profile membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs - pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + let chatSettings = ChatSettings {enableNtfs = True} + pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo @@ -1345,12 +1353,13 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInv profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (profileId, localDisplayName, connRequest, userId, currentTs, currentTs) + "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (profileId, localDisplayName, connRequest, userId, True, currentTs, currentTs) insertedRowId db _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs - pure GroupInfo {groupId, localDisplayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + let chatSettings = ChatSettings {enableNtfs = True} + pure GroupInfo {groupId, localDisplayName, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do @@ -1461,7 +1470,7 @@ getUserGroupDetails db User {userId, userContactId} = <$> DB.query db [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, mp.contact_profile_id, mp.display_name, mp.full_name, mp.image FROM groups g @@ -1477,12 +1486,13 @@ getGroupInfoByName db user gName = do gId <- getGroupIdByName db user gName getGroupInfo db user gId -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, UTCTime, UTCTime) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, createdAt, updatedAt) :. userMemberRow) = +toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. userMemberRow) = let membership = toGroupMember userContactId userMemberRow - in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, chatSettings, createdAt, updatedAt} getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db user@User {userId} groupId groupMemberId = @@ -1882,7 +1892,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1922,7 +1932,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = db [sql| SELECT - ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct @@ -1938,11 +1948,12 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow -> Contact - toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow) = + toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact + toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} activeConn = toConnection connRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} createSndFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> IO Int64 createSndFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize} acId chunkSize = do @@ -2680,7 +2691,7 @@ getDirectChatPreviews_ db User {userId} = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, @@ -2745,7 +2756,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -2875,6 +2886,14 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) +updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () +updateContactSettings db User {userId} contactId ChatSettings {enableNtfs} = + DB.execute db "UPDATE contacts SET enable_ntfs = ? WHERE user_id = ? AND contact_id = ?" (enableNtfs, userId, contactId) + +updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () +updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs} = + DB.execute db "UPDATE groups SET enable_ntfs = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, userId, groupId) + toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, UTCTime, UTCTime) -> PendingContactConnection toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt) = PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt} @@ -3012,7 +3031,7 @@ getContact db userId contactId = [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -3137,7 +3156,7 @@ getGroupInfo db User {userId, userContactId} groupId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 95d01d2a20..141936838a 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -76,6 +76,7 @@ data Contact = Contact profile :: LocalProfile, activeConn :: Connection, viaGroup :: Maybe Int64, + chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -190,6 +191,7 @@ data GroupInfo = GroupInfo localDisplayName :: GroupName, groupProfile :: GroupProfile, membership :: GroupMember, + chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -200,6 +202,17 @@ instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOption groupName' :: GroupInfo -> GroupName groupName' GroupInfo {localDisplayName = g} = g +-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties) +data ChatSettings = ChatSettings + { enableNtfs :: Bool + } + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOptions + +defaultChatSettings :: ChatSettings +defaultChatSettings = ChatSettings {enableNtfs = True} + data Profile = Profile { displayName :: ContactName, fullName :: Text, diff --git a/stack.yaml b/stack.yaml index f199a4225f..0bbcb4b8eb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 3fc9afbb351033413dd84344ec603910e370a8ec + commit: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 992ba7530653a2c0ee41e3efa61d8aa271bf6248 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:32:55 +0100 Subject: [PATCH 31/89] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index e73877f169..4c3cb44478 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd + tag: 212a9bc4ab5fd4b6f6c80f8e0733509e631d3183 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 7d06411d5c..6ea649b4a4 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."3fc9afbb351033413dd84344ec603910e370a8ec" = "1zkjlrqffyv90wxsxjx73zmksrv79bvg7hklzmp1iac5pxqvdjfv"; + "https://github.com/simplex-chat/simplexmq.git"."212a9bc4ab5fd4b6f6c80f8e0733509e631d3183" = "1lnckc41f32023i8g4k36sykmdj6jhx2327zsz3c6bzzl9xn44vd"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 0bbcb4b8eb..1c00bd3efe 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd + commit: 212a9bc4ab5fd4b6f6c80f8e0733509e631d3183 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From fee2b247e92633d29219dae84f3a93b43fd80f22 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Aug 2022 16:02:08 +0100 Subject: [PATCH 32/89] android: update search api (#948) --- .../app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index fe5008e4db..a9684da5a6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1172,7 +1172,7 @@ sealed class CC { is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" is ApiGetChats -> "/_get chats pcc=on" - is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " $search") + is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}" is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" From 8c716962fb1049b8d3a847413ad5304928ddb397 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 19 Aug 2022 19:16:52 +0300 Subject: [PATCH 33/89] Different level of APK compression (#947) * Different level of APK compression - can reduce from 200mb to 50mb with level 5 of compression. Supports Intellij IDEA and command line gradle invocation - by default, this feature is disabled. To enable create a file local.properties in `apps/android` and paste this line: `compression_level=5` - level can be from 0 (no compression) to 9 (slowest and the must effective) - automatically enables `extractNativeLibs` AndroidManifest's flag since it's required in this case. Feel free to find an alternative that works with compression of .so libs and without enabling this flag - Windows is not suppored, of course. Only Unix-like OSes * script corrections * Missing JAVA_HOME in some environments * Rename release apk made by IDEA to simplex.apk * Enhancements Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/android/app/build.gradle | 69 +++++++++++++++++++ apps/android/app/src/main/AndroidManifest.xml | 1 + apps/android/build.gradle | 3 + scripts/android/compress-and-sign-apk.sh | 45 ++++++++++++ 4 files changed, 118 insertions(+) create mode 100755 scripts/android/compress-and-sign-apk.sh diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 45b0cd177c..3eb85d5924 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -28,6 +28,7 @@ android { } manifestPlaceholders.app_name = "@string/app_name" manifestPlaceholders.provider_authorities = "chat.simplex.app.provider" + manifestPlaceholders.extract_native_libs = compression_level != "0" } buildTypes { @@ -73,6 +74,7 @@ android { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } + jniLibs.useLegacyPackaging = compression_level != "0" } } @@ -122,3 +124,70 @@ dependencies { androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" } + +def buildType = "unknown" +// Don't do anything if no compression is needed +if (compression_level != "0") { + tasks.whenTaskAdded { task -> + if (task.name == 'packageDebug') { + task.doLast { + buildType = "debug" + } + task.finalizedBy compressApk + } else if (task.name == 'packageRelease') { + task.doLast { + buildType = "release" + } + task.finalizedBy compressApk + } + } +} + +tasks.register("compressApk") { + doLast { + def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome() + def sdkDir = android.getSdkDirectory().getAbsolutePath() + def keyAlias = "" + def keyPassword = "" + def storeFile = "" + def storePassword = "" + if (project.properties['android.injected.signing.key.alias'] != null) { + keyAlias = project.properties['android.injected.signing.key.alias'] + keyPassword = project.properties['android.injected.signing.key.password'] + storeFile = project.properties['android.injected.signing.store.file'] + storePassword = project.properties['android.injected.signing.store.password'] + } else if (android.signingConfigs.hasProperty(buildType)) { + def gradleConfig = android.signingConfigs[buildType] + keyAlias = gradleConfig.keyAlias + keyPassword = gradleConfig.keyPassword + storeFile = gradleConfig.storeFile + storePassword = gradleConfig.storePassword + } else { + // There is no signing config for current build type, can't sign the apk + println("No signing configs for this build type: $buildType") + return + } + + def outputDir = tasks["package${buildType.capitalize()}"].outputs.files.last() + + exec { + workingDir '../../../scripts/android' + setEnvironment(['JAVA_HOME': "$javaHome"]) + commandLine './compress-and-sign-apk.sh', \ + "$compression_level", \ + "$outputDir", \ + "$sdkDir", \ + "$storeFile", \ + "$storePassword", \ + "$keyAlias", \ + "$keyPassword" + } + + if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') { + new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk")) + } + + // View all gradle properties set + // project.properties.each { k, v -> println "$k -> $v" } + } +} diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 127641dc54..3f04b64fcb 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:allowBackup="true" android:icon="@mipmap/icon" android:label="${app_name}" + android:extractNativeLibs="${extract_native_libs}" android:supportsRtl="true" android:theme="@style/Theme.SimpleX"> diff --git a/apps/android/build.gradle b/apps/android/build.gradle index 552ed8c8f1..e7be963f32 100644 --- a/apps/android/build.gradle +++ b/apps/android/build.gradle @@ -18,6 +18,9 @@ buildscript { // to allow debug & release versions to coexist application_id_suffix = localProperties['application_id_suffix'] ?: '' + // Compression level for debug AND release apk. 0 = disable compression. Max is 9 + compression_level = localProperties['compression_level'] ?: '0' + // NOTE: If you need a different version of something, provide it in `local.properties` // like so: compose_version=123, or gradle_plugin_version=1.2.3, etc } diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh new file mode 100755 index 0000000000..c07a640fbf --- /dev/null +++ b/scripts/android/compress-and-sign-apk.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Fail fast in case any command fails +set -e + +level=$1 +apk_parent_dir=$2 +sdk_dir=$3 + +store_file=$4 +store_password=$5 +key_alias=$6 +key_password=$7 + +if [ -z ${7} ]; then echo "You didn't enter all required params: +compress-and-sign-apk.sh level apk_parent_dir sdk_dir store_file store_password key_alias key_password" +fi + +cd $apk_parent_dir + +ORIG_NAME=$(echo app*.apk) +unzip -o -q -d apk $ORIG_NAME + +rm $ORIG_NAME + +(cd apk && zip -r -q -$level ../$ORIG_NAME .) +# Shouldn't be compressed because of Android requirement +(cd apk && zip -r -q -0 ../$ORIG_NAME resources.arsc) +#(cd apk && 7z a -r -mx=$level -tzip -x!resources.arsc ../$ORIG_NAME .) +#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) + +ALL_TOOLS=($sdk_dir/build-tools/*/) +BIN_DIR="${ALL_TOOLS[1]}" + +$BIN_DIR/zipalign -p -f 4 $ORIG_NAME $ORIG_NAME-2 + +mv $ORIG_NAME{-2,} + +$BIN_DIR/apksigner sign \ + --ks "$store_file" --ks-key-alias "$key_alias" --ks-pass "pass:$store_password" \ + --key-pass "pass:$key_password" $ORIG_NAME + +# cleanup +rm -rf apk || true +rm ${ORIG_NAME}.idsig 2> /dev/null || true From 51b8ce10ae3a3349070edf8a66827a00a2d23c96 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 19 Aug 2022 19:17:02 +0300 Subject: [PATCH 34/89] Better performance in FloatingButtons function (#949) --- .../chat/simplex/app/views/chat/ChatView.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index fd297bcfbd..5006305731 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -536,16 +536,36 @@ fun BoxWithConstraintsScope.FloatingButtons( listState: LazyListState ) { val scope = rememberCoroutineScope() + + var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } + var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } + var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) } + + LaunchedEffect(listState) { + snapshotFlow { listState.firstVisibleItemIndex } + .distinctUntilChanged() + .collect { + firstVisibleIndex = it + firstItemIsVisible = firstVisibleIndex == 0 + } + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } + .distinctUntilChanged() + .collect { + lastIndexOfVisibleItems = it + } + } + val bottomUnreadCount by remember { derivedStateOf { - val from = chatItems.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex + if (unreadCount.value == 0) return@derivedStateOf 0 + + val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems if (chatItems.size <= from || from < 0) return@derivedStateOf 0 chatItems.subList(from, chatItems.size).count { it.isRcvNew } } } - val firstItemIsVisible by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { From 74966b1425a7b5b25c8b3249f16bbb85c5baf831 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 19 Aug 2022 22:02:28 +0300 Subject: [PATCH 35/89] Two fixes, better performance too (#951) - on orientation change scroll position in chat wasn't preserved - the app had been making multiple same queries to a database when tried to preload more messages --- .../chat/simplex/app/views/chat/ChatView.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 5006305731..98926ad61a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -429,8 +429,14 @@ fun BoxWithConstraintsScope.ChatItemsList( // Don't autoscroll next time until it will be needed shouldAutoScroll = false } + var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } // Scroll to bottom when search value changes from something to nothing and back LaunchedEffect(searchValue.value.isEmpty()) { + // They are equal when orientation was changed, don't need to scroll. + // LaunchedEffect unaware of this event since it uses remember, not rememberSaveable + if (prevSearchEmptiness == searchValue.value.isEmpty()) return@LaunchedEffect + prevSearchEmptiness = searchValue.value.isEmpty() + if (listState.firstVisibleItemIndex != 0) { scope.launch { listState.scrollToItem(0) } } @@ -632,20 +638,16 @@ fun PreloadItems( items: List<*>, onLoadMore: (chat: Chat) -> Unit, ) { - val loadMore = remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - if (lastVisibleItemIndex > (totalItemsNumber - remaining)) - totalItemsNumber - else - 0 - } - } - - LaunchedEffect(loadMore, chat, items) { - snapshotFlow { loadMore.value } + LaunchedEffect(listState, chat, items) { + snapshotFlow { listState.layoutInfo } + .map { + val totalItemsNumber = it.totalItemsCount + val lastVisibleItemIndex = (it.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + if (lastVisibleItemIndex > (totalItemsNumber - remaining)) + totalItemsNumber + else + 0 + } .distinctUntilChanged() .filter { it > 0 } .collect { From f561698fb9b14778e4f414e38c3e9a03d46205f6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Aug 2022 20:36:29 +0100 Subject: [PATCH 36/89] mobile: update version: 3.2, android 49 / iOS 69 --- apps/android/app/build.gradle | 4 ++-- apps/ios/Shared/Views/Chat/ChatView.swift | 1 - apps/ios/SimpleX.xcodeproj/project.pbxproj | 24 +++++++++++----------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 3eb85d5924..4765f58c5e 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 48 - versionName "3.1" + versionCode 49 + versionName "3.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d09e2ebd29..ffce1b86fb 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -8,7 +8,6 @@ import SwiftUI import SimpleXChat -import Introspect private let memberImageSize: CGFloat = 34 diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index cab6545152..77a5450424 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1160,7 +1160,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1181,7 +1181,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1202,7 +1202,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1223,7 +1223,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1281,7 +1281,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1294,7 +1294,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1311,7 +1311,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1324,7 +1324,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1342,7 +1342,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1366,7 +1366,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1388,7 +1388,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 69; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1412,7 +1412,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 3.1; + MARKETING_VERSION = 3.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; From d26083d8b795216d40e02a8be790dc9062453240 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Aug 2022 22:44:00 +0100 Subject: [PATCH 37/89] core: fix settings api (#952) --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a28f43f61f..3043bb8cae 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2597,7 +2597,7 @@ chatCommandP = "/_network " *> (APISetNetworkConfig <$> jsonP), ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, - "/_settings" *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), + "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), "/_info @" *> (APIContactInfo <$> A.decimal), ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName), From e6233722db36df863f858a12a21094ea3f0c3406 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:12:20 +0400 Subject: [PATCH 38/89] core: create incognito membership if direct connection with host is incognito when processing invitation instead of on join (#953) --- src/Simplex/Chat.hs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3043bb8cae..1d324120c7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -760,16 +760,10 @@ processChatCommand = \case APIJoinGroup groupId -> withUser $ \user@User {userId} -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId withChatLock . procCmd $ do - -- [incognito] if (incognito mode is enabled OR direct connection with host is incognito) [AND membership is not incognito] update membership to use incognito profile + -- [incognito] if incognito mode is enabled [AND membership is not incognito] update membership to use incognito profile incognito <- readTVarIO =<< asks incognitoMode - hostConnIncognito <- case (incognito, memberContactId fromMember) of - (True, _) -> pure True -- we don't need to check whether connection with host is incognito if incognito mode is on - (_, Just hostContactId) -> do - hostContact <- withStore $ \db -> getContact db userId hostContactId - pure $ contactConnIncognito hostContact - _ -> pure False g'@GroupInfo {membership = membership'} <- - if (incognito || hostConnIncognito) && not (memberIncognito membership) + if incognito && not (memberIncognito membership) then do incognitoProfile <- liftIO generateRandomProfile membership' <- withStore $ \db -> createMemberIncognitoProfile db userId membership (Just incognitoProfile) @@ -1916,15 +1910,16 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () - processGroupInvitation ct@Contact {localDisplayName = c} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), fromMemberProfile, invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do + processGroupInvitation ct@Contact {contactId, localDisplayName = c} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), fromMemberProfile, invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId - -- [incognito] if received group invitation has host's incognito profile, create membership with new incognito profile; incognito mode is checked when joining group - let invitedIncognito = isJust fromMemberProfile - incognitoProfile <- if invitedIncognito then Just <$> liftIO generateRandomProfile else pure Nothing + -- [incognito] if (received group invitation has host's incognito profile OR direct connection with host is incognito), create membership with new incognito profile; incognito mode is checked when joining group + hostContact <- withStore $ \db -> getContact db userId contactId + let joinGroupIncognito = isJust fromMemberProfile || contactConnIncognito hostContact + incognitoProfile <- if joinGroupIncognito then Just <$> liftIO generateRandomProfile else pure Nothing gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv incognitoProfile - let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just invitedIncognito}) memRole + let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just joinGroupIncognito}) memRole ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci From 307db450d8c73ec42d1d3275b8ef519aa775bcc5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 20 Aug 2022 12:47:48 +0100 Subject: [PATCH 39/89] ios: mute notifications per chat (#950) * mute notifications per chat * toggle notifications * update settings api * move model changes to main thread * add mute indication, remove swipe buttons * icon Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/ios/Shared/Model/NtfManager.swift | 4 +- apps/ios/Shared/Model/SimpleXAPI.swift | 4 ++ apps/ios/Shared/Views/Chat/ChatView.swift | 36 +++++++++++++++++ .../Views/ChatList/ChatListNavLink.swift | 10 +---- .../Views/ChatList/ChatPreviewView.swift | 6 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++---------- apps/ios/SimpleXChat/APITypes.swift | 4 ++ apps/ios/SimpleXChat/ChatTypes.swift | 14 ++++++- 8 files changed, 85 insertions(+), 33 deletions(-) diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 114baa36b2..0c36e58d0c 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -200,7 +200,9 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) { logger.debug("NtfManager.notifyMessageReceived") - addNotification(createMessageReceivedNtf(cInfo, cItem)) + if cInfo.ntfsEnabled { + addNotification(createMessageReceivedNtf(cInfo, cItem)) + } } func notifyCallInvitation(_ invitation: RcvCallInvitation) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index bdd037e769..84f8699074 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -308,6 +308,10 @@ func setNetworkConfig(_ cfg: NetCfg) throws { throw r } +func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws { + try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings)) +} + func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? { let r = await chatSendCmd(.apiContactInfo(contactId: contactId)) if case let .contactInfo(_, connStats) = r { return connStats } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ffce1b86fb..5bfd2d98b6 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -118,6 +118,7 @@ struct ChatView: View { Label("Video call", systemImage: "video") } searchButton() + toggleNtfsButton(chat) } label: { Image(systemName: "ellipsis") } @@ -132,6 +133,7 @@ struct ChatView: View { } Menu { searchButton() + toggleNtfsButton(chat) } label: { Image(systemName: "ellipsis") } @@ -502,6 +504,40 @@ struct ChatView: View { } } +@ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View { + Button { + toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + } label: { + if chat.chatInfo.ntfsEnabled { + Label("Mute", systemImage: "speaker.slash") + } else { + Label("Unmute", systemImage: "speaker.wave.2") + } + } +} + +func toggleNotifications(_ chat: Chat, enableNtfs: Bool) { + Task { + do { + let chatSettings = ChatSettings(enableNtfs: enableNtfs) + try await apiSetChatSettings(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, chatSettings: chatSettings) + await MainActor.run { + switch chat.chatInfo { + case var .direct(contact): + contact.chatSettings = chatSettings + ChatModel.shared.updateContact(contact) + case var .group(groupInfo): + groupInfo.chatSettings = chatSettings + ChatModel.shared.updateGroup(groupInfo) + default: () + } + } + } catch let error { + logger.error("apiSetChatSettings error \(responseError(error))") + } + } +} + struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index ecf978e67c..74756bfabd 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -41,15 +41,13 @@ struct ChatListNavLink: View { label: { ChatPreviewView(chat: chat) }, disabled: !contact.ready ) - .swipeActions(edge: .leading) { + .swipeActions(edge: .leading, allowsFullSwipe: true) { if chat.chatStats.unreadCount > 0 { markReadButton() } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { clearChatButton() - } - .swipeActions(edge: .trailing) { Button(role: .destructive) { AlertManager.shared.showAlert( contact.ready @@ -78,8 +76,6 @@ struct ChatListNavLink: View { .frame(height: 80) .swipeActions(edge: .trailing, allowsFullSwipe: true) { joinGroupButton() - } - .swipeActions(edge: .trailing) { if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } @@ -104,15 +100,13 @@ struct ChatListNavLink: View { disabled: !groupInfo.ready ) .frame(height: 80) - .swipeActions(edge: .leading) { + .swipeActions(edge: .leading, allowsFullSwipe: true) { if chat.chatStats.unreadCount > 0 { markReadButton() } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { clearChatButton() - } - .swipeActions(edge: .trailing) { if (groupInfo.membership.memberCurrent) { Button { AlertManager.shared.showAlert(leaveGroupAlert(groupInfo)) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 84819a92b6..aca5e9e44a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -36,7 +36,6 @@ struct ChatPreviewView: View { .frame(minWidth: 60, alignment: .trailing) .foregroundColor(.secondary) .padding(.top, 4) - } .padding(.top, 4) .padding(.horizontal, 8) @@ -113,8 +112,11 @@ struct ChatPreviewView: View { .foregroundColor(.white) .padding(.horizontal, 4) .frame(minWidth: 18, minHeight: 18) - .background(Color.accentColor) + .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary) .cornerRadius(10) + } else if !chat.chatInfo.ntfsEnabled { + Image(systemName: "speaker.slash.fill") + .foregroundColor(.secondary) } } } else { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 77a5450424..5f435b04a4 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -13,11 +13,11 @@ 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; - 5C00164C28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */; }; - 5C00164D28ACEA380094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164828ACEA380094D739 /* libgmp.a */; }; - 5C00164E28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */; }; - 5C00164F28ACEA380094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164A28ACEA380094D739 /* libffi.a */; }; - 5C00165028ACEA380094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00164B28ACEA380094D739 /* libgmpxx.a */; }; + 5C00165628B02AF40094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165128B02AF30094D739 /* libffi.a */; }; + 5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */; }; + 5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */; }; + 5C00165928B02AF40094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165428B02AF30094D739 /* libgmp.a */; }; + 5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165528B02AF30094D739 /* libgmpxx.a */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -196,11 +196,11 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; - 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a"; sourceTree = ""; }; - 5C00164828ACEA380094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a"; sourceTree = ""; }; - 5C00164A28ACEA380094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C00164B28ACEA380094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C00165128B02AF30094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a"; sourceTree = ""; }; + 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a"; sourceTree = ""; }; + 5C00165428B02AF30094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C00165528B02AF30094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -350,12 +350,12 @@ buildActionMask = 2147483647; files = ( 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C00164F28ACEA380094D739 /* libffi.a in Frameworks */, + 5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */, + 5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */, + 5C00165628B02AF40094D739 /* libffi.a in Frameworks */, + 5C00165928B02AF40094D739 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C00164E28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a in Frameworks */, - 5C00164D28ACEA380094D739 /* libgmp.a in Frameworks */, - 5C00164C28ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a in Frameworks */, - 5C00165028ACEA380094D739 /* libgmpxx.a in Frameworks */, + 5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -410,11 +410,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C00164A28ACEA380094D739 /* libffi.a */, - 5C00164828ACEA380094D739 /* libgmp.a */, - 5C00164B28ACEA380094D739 /* libgmpxx.a */, - 5C00164728ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz-ghc8.10.7.a */, - 5C00164928ACEA380094D739 /* libHSsimplex-chat-3.1.0-iRSBBAuo4W6e6BG2FodFz.a */, + 5C00165128B02AF30094D739 /* libffi.a */, + 5C00165428B02AF30094D739 /* libgmp.a */, + 5C00165528B02AF30094D739 /* libgmpxx.a */, + 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */, + 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 64ee7b6970..486139e169 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -597,6 +597,10 @@ public struct KeepAliveOpts: Codable, Equatable { public struct ChatSettings: Codable { public var enableNtfs: Bool + public init(enableNtfs: Bool) { + self.enableNtfs = enableNtfs + } + public static let defaults: ChatSettings = ChatSettings(enableNtfs: true) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index fd82605e65..267d27d03a 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -189,6 +189,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var ntfsEnabled: Bool { + switch self { + case let .direct(contact): return contact.chatSettings.enableNtfs + case let .group(groupInfo): return groupInfo.chatSettings.enableNtfs + default: return false + } + } + var createdAt: Date { switch self { case let .direct(contact): return contact.createdAt @@ -244,7 +252,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var profile: Profile public var activeConn: Connection public var viaGroup: Int64? -// public var chatSettings: ChatSettings + public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date @@ -265,6 +273,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { localDisplayName: "alice", profile: Profile.sampleData, activeConn: Connection.sampleData, + chatSettings: ChatSettings.defaults, createdAt: .now, updatedAt: .now ) @@ -434,7 +443,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { var localDisplayName: GroupName public var groupProfile: GroupProfile public var membership: GroupMember -// public var chatSettings: ChatSettings + public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date @@ -463,6 +472,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { localDisplayName: "team", groupProfile: GroupProfile.sampleData, membership: GroupMember.sampleData, + chatSettings: ChatSettings.defaults, createdAt: .now, updatedAt: .now ) From 164426db49d0d89242433b0b3f99d4c414dd3514 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 20 Aug 2022 14:47:24 +0100 Subject: [PATCH 40/89] core: catch error when toggling notifications (#954) * core: catch error when toggling notifications * filter current members * filter active members --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 4 ++-- stack.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index 4c3cb44478..27b2d7cb31 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 212a9bc4ab5fd4b6f6c80f8e0733509e631d3183 + tag: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6ea649b4a4..76ea520034 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."212a9bc4ab5fd4b6f6c80f8e0733509e631d3183" = "1lnckc41f32023i8g4k36sykmdj6jhx2327zsz3c6bzzl9xn44vd"; + "https://github.com/simplex-chat/simplexmq.git"."a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef" = "0iqk58dhckpij9l1z8bm83hghw5cwj9hmpkbk7j8vws123g1bd73"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 1d324120c7..78a6b1c113 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -610,8 +610,8 @@ processChatCommand = \case Group _ ms <- getGroup db user chatId liftIO $ updateGroupSettings db user chatId chatSettings pure ms - withAgent $ \a -> forM_ ms $ \m -> - forM_ (memberConnId m) $ \connId -> toggleConnectionNtfs a connId (enableNtfs chatSettings) + forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> + withAgent (\a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings) `catchError` (toView . CRChatError) pure CRCmdOk _ -> pure $ chatCmdError "not supported" APIContactInfo contactId -> withUser $ \User {userId} -> do diff --git a/stack.yaml b/stack.yaml index 1c00bd3efe..4b4865916f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 212a9bc4ab5fd4b6f6c80f8e0733509e631d3183 + commit: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 9f94c6f98a160a836151853d31d54c0034f08c22 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:52:25 +0100 Subject: [PATCH 41/89] 3.2.0 (#957) --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 7e5aa0834d..893402be70 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 3.1.0 +version: 3.2.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index cdd4cdf838..fe5cfc8b25 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 3.1.0 +version: 3.2.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 2cffe91e0bba8e7ff3f67d89a03392a421178196 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 20 Aug 2022 21:55:06 +0100 Subject: [PATCH 42/89] ios: choose accent color (#956) --- apps/ios/Shared/AppDelegate.swift | 20 ++++++++++ .../UserSettings/AppearanceSettings.swift | 40 +++++++++++++++++++ .../Views/UserSettings/SettingsView.swift | 8 +++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index b06fac9ee0..068693461b 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -80,6 +80,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { terminateChat() } + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + if connectingSceneSession.role == .windowApplication { + configuration.delegateClass = SceneDelegate.self + } + return configuration + } + private func receiveMessages(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { let complete = BGManager.shared.completionHandler { logger.debug("AppDelegate: completed BGManager.receiveMessages") @@ -89,3 +99,13 @@ class AppDelegate: NSObject, UIApplicationDelegate { BGManager.shared.receiveMessages(complete) } } + +class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + window = windowScene.keyWindow + window?.tintColor = UIColor(cgColor: getUIAccentColorDefault()) + } +} diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 1c0558b86b..98aa97fec7 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -8,9 +8,13 @@ import SwiftUI +let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1) + struct AppearanceSettings: View { + @EnvironmentObject var sceneDelegate: SceneDelegate @State private var iconLightTapped = false @State private var iconDarkTapped = false + @State private var uiTintColor = getUIAccentColorDefault() var body: some View { VStack{ @@ -22,6 +26,23 @@ struct AppearanceSettings: View { updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped) } } + + Section { + ColorPicker("Accent color", selection: $uiTintColor, supportsOpacity: false) + } header: { + Text("Colors") + } footer: { + Button { + uiTintColor = defaultAccentColor + setUIAccentColorDefault(defaultAccentColor) + } label: { + Text("Reset colors").font(.callout) + } + } + .onChange(of: uiTintColor) { _ in + sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor) + setUIAccentColorDefault(uiTintColor) + } } } } @@ -44,6 +65,25 @@ struct AppearanceSettings: View { } } +func getUIAccentColorDefault() -> CGColor { + let defs = UserDefaults.standard + return CGColor( + red: defs.double(forKey: DEFAULT_ACCENT_COLOR_RED), + green: defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN), + blue: defs.double(forKey: DEFAULT_ACCENT_COLOR_BLUE), + alpha: 1 + ) +} + +func setUIAccentColorDefault(_ color: CGColor) { + if let cs = color.components { + let defs = UserDefaults.standard + defs.set(cs[0], forKey: DEFAULT_ACCENT_COLOR_RED) + defs.set(cs[1], forKey: DEFAULT_ACCENT_COLOR_GREEN) + defs.set(cs[2], forKey: DEFAULT_ACCENT_COLOR_BLUE) + } +} + struct AppearanceSettings_Previews: PreviewProvider { static var previews: some View { AppearanceSettings() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index dd2a592c4a..bd87e77ec2 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -26,6 +26,9 @@ let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName" let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime" let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration" let DEFAULT_DEVELOPER_TOOLS = "developerTools" +let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" +let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" +let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, @@ -36,7 +39,10 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_LINK_PREVIEWS: true, DEFAULT_EXPERIMENTAL_CALLS: false, DEFAULT_CHAT_V3_DB_MIGRATION: "offer", - DEFAULT_DEVELOPER_TOOLS: false + DEFAULT_DEVELOPER_TOOLS: false, + DEFAULT_ACCENT_COLOR_RED: 0.000, + DEFAULT_ACCENT_COLOR_GREEN: 0.533, + DEFAULT_ACCENT_COLOR_BLUE: 1.000 ] private var indent: CGFloat = 36 From 0a048eb2864eed620dc6c00fb8801bbcab3124fd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 20 Aug 2022 23:03:56 +0100 Subject: [PATCH 43/89] ios: fix typo, add information to settings (#958) --- apps/ios/Shared/Views/UserSettings/CallSettings.swift | 10 +++++++++- .../Shared/Views/UserSettings/NetworkAndServers.swift | 6 +++++- apps/ios/SimpleXChat/APITypes.swift | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index 7bc79dd0b3..2406eb17f4 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -14,8 +14,16 @@ struct CallSettings: View { var body: some View { VStack { List { - Section("Settings") { + Section { Toggle("Connect via relay", isOn: $webrtcPolicyRelay) + } header: { + Text("Settings") + } footer: { + if webrtcPolicyRelay { + Text("Relay server protects your IP address, but it can observe the duration of the call.") + } else { + Text("Relay server is only used if necessary. Another party can observe your IP address.") + } } Section("Limitations") { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index 6b91c02391..10ffde6a1e 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -32,7 +32,7 @@ struct NetworkAndServers: View { var body: some View { VStack { List { - Section("") { + Section { NavigationLink { SMPServers() .navigationTitle("Your SMP servers") @@ -52,6 +52,10 @@ struct NetworkAndServers: View { settingsRow("app.connected.to.app.below.fill") { Text("Advanced network settings") } } } + } header: { + Text("") + } footer: { + Text("Using .onion hosts requires compatible VPN provider.") } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 486139e169..83ca4bb955 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -561,7 +561,7 @@ public enum OnionHosts: String, Identifiable { switch self { case .no: return "No" case .prefer: return "When available" - case .require: return "Requred" + case .require: return "Required" } } From 985f3dffd3955c2fdada1f8c22202c657b16e655 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 22 Aug 2022 11:04:34 +0400 Subject: [PATCH 44/89] core: read host's connection custom_user_profile_id into group info (#960) --- src/Simplex/Chat/Store.hs | 46 ++++++++++++++++++++++++--------------- src/Simplex/Chat/Types.hs | 1 + 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 74ca466072..3f28cb8e54 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1179,7 +1179,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1193,6 +1193,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? |] @@ -1267,7 +1269,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1283,6 +1285,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) @@ -1325,11 +1329,11 @@ createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = Except -- TODO ldn from incognito profile membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs let chatSettings = ChatSettings {enableNtfs = True} - pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} + pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo -createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, fromMemberProfile, invitedMember, connRequest, groupProfile} incognitoProfile = do +createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, fromMemberProfile, invitedMember, connRequest, groupProfile} incognitoProfile = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ -- TODO treat the case that the invitation details could've changed @@ -1359,7 +1363,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInv _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs let chatSettings = ChatSettings {enableNtfs = True} - pure GroupInfo {groupId, localDisplayName, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} + pure GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs} createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do @@ -1470,14 +1474,16 @@ getUserGroupDetails db User {userId, userContactId} = <$> DB.query db [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, - m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, mp.contact_profile_id, mp.display_name, mp.full_name, mp.image + SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, + mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image FROM groups g JOIN group_profiles gp USING (group_profile_id) - JOIN group_members m USING (group_id) - JOIN contact_profiles mp ON mp.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) - WHERE g.user_id = ? AND m.contact_id = ? + JOIN group_members mu USING (group_id) + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id + WHERE g.user_id = ? AND mu.contact_id = ? |] (userId, userContactId) @@ -1486,13 +1492,13 @@ getGroupInfoByName db user gName = do gId <- getGroupIdByName db user gName getGroupInfo db user gId -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. userMemberRow) = +toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, createdAt, updatedAt) :. userMemberRow) = let membership = toGroupMember userContactId userMemberRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} - in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, chatSettings, createdAt, updatedAt} + in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt} getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db user@User {userId} groupId groupMemberId = @@ -1892,7 +1898,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1909,6 +1915,8 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) @@ -2756,7 +2764,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -2780,6 +2788,8 @@ getGroupChatPreviews_ db User {userId, userContactId} = do FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN ( SELECT group_id, MAX(chat_item_id) AS MaxId @@ -3156,7 +3166,7 @@ getGroupInfo db User {userId, userContactId} groupId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -3164,6 +3174,8 @@ getGroupInfo db User {userId, userContactId} groupId = FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id + LEFT JOIN contacts h ON h.contact_id = mu.invited_by + LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? |] diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 141936838a..87ce90f62c 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -191,6 +191,7 @@ data GroupInfo = GroupInfo localDisplayName :: GroupName, groupProfile :: GroupProfile, membership :: GroupMember, + hostConnCustomUserProfileId :: Maybe ProfileId, chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime From 3c2f5d14f562e3b29f1ab0e264755cf981e51fe9 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 22 Aug 2022 14:14:46 +0300 Subject: [PATCH 45/89] Made scrolling faster (#961) --- .../chat/simplex/app/views/chat/ChatView.kt | 193 +++++++++--------- 1 file changed, 99 insertions(+), 94 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 98926ad61a..841c858732 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -252,24 +252,22 @@ fun ChatLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } + val setFloatingButton = { button: @Composable () -> Unit -> + floatingButton.value = button + } + Scaffold( topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers, onSearchValueChanged) }, bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding(), - floatingActionButton = floatingButton.value, + floatingActionButton = { floatingButton.value() }, ) { contentPadding -> - CompositionLocalProvider( - // Makes horizontal and vertical scrolling to coexist nicely. - // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view - LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() - ) { - BoxWithConstraints(Modifier.padding(contentPadding)) { - ChatItemsList( - user, chat, unreadCount, composeState, chatItems, searchValue, - useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, - receiveFile, joinGroup, acceptCall, markRead, floatingButton - ) - } + BoxWithConstraints(Modifier.padding(contentPadding)) { + ChatItemsList( + user, chat, unreadCount, composeState, chatItems, searchValue, + useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, + receiveFile, joinGroup, acceptCall, markRead, setFloatingButton + ) } } } @@ -412,7 +410,7 @@ fun BoxWithConstraintsScope.ChatItemsList( joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - floatingButton: MutableState<@Composable () -> Unit> + setFloatingButton: (@Composable () -> Unit) -> Unit, ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -451,84 +449,90 @@ fun BoxWithConstraintsScope.ChatItemsList( val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } } LazyColumn(state = listState, reverseLayout = true) { itemsIndexed(reversedChatItems) { i, cItem -> - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } - val directions = setOf(DismissDirection.EndToStart) - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = directions, - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) - val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) - if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { - LaunchedEffect(Unit) { - scope.launch { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } - } - } - } - - if (chat.chatInfo is ChatInfo.Group) { - if (cItem.chatDir is CIDirection.GroupRcv) { - val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null - val member = cItem.chatDir.groupMember - val showMember = showMemberImage(member, prevItem) - Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) { - if (showMember) { - val contactId = member.memberContactId - if (contactId == null) { - MemberImage(member) + CompositionLocalProvider( + // Makes horizontal and vertical scrolling to coexist nicely. + // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view + LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() + ) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } + val directions = setOf(DismissDirection.EndToStart) + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = directions, + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) + val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) + val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) + if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { + LaunchedEffect(Unit) { + scope.launch { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) } else { - Box( - Modifier - .clip(CircleShape) - .clickable { - openDirectChat(contactId) - // Scroll to first unread message when direct chat will be loaded - shouldAutoScroll = true - } - ) { - MemberImage(member) - } + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) } - Spacer(Modifier.size(4.dp)) - } else { - Spacer(Modifier.size(42.dp)) } - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) - } - } else { - Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) { - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) } } - } else { // direct message - val sent = cItem.chatDir.sent - Box( - Modifier.padding( - start = if (sent) 76.dp else 12.dp, - end = if (sent) 12.dp else 76.dp, - ).then(swipeableModifier) - ) { - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) - } - } - if (cItem.isRcvNew) { - LaunchedEffect(cItem.id) { - scope.launch { - delay(750) - markRead(CC.ItemRange(cItem.id, cItem.id), null) + if (chat.chatInfo is ChatInfo.Group) { + if (cItem.chatDir is CIDirection.GroupRcv) { + val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null + val member = cItem.chatDir.groupMember + val showMember = showMemberImage(member, prevItem) + Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) { + if (showMember) { + val contactId = member.memberContactId + if (contactId == null) { + MemberImage(member) + } else { + Box( + Modifier + .clip(CircleShape) + .clickable { + openDirectChat(contactId) + // Scroll to first unread message when direct chat will be loaded + shouldAutoScroll = true + } + ) { + MemberImage(member) + } + } + Spacer(Modifier.size(4.dp)) + } else { + Spacer(Modifier.size(42.dp)) + } + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) + } + } else { + Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) { + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) + } + } + } else { // direct message + val sent = cItem.chatDir.sent + Box( + Modifier.padding( + start = if (sent) 76.dp else 12.dp, + end = if (sent) 12.dp else 76.dp, + ).then(swipeableModifier) + ) { + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) + } + } + + if (cItem.isRcvNew) { + LaunchedEffect(cItem.id) { + scope.launch { + delay(750) + markRead(CC.ItemRange(cItem.id, cItem.id), null) + } } } } } } - FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, floatingButton, listState) + FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) } @Composable @@ -538,7 +542,7 @@ fun BoxWithConstraintsScope.FloatingButtons( minUnreadItemId: Long, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - floatingButton: MutableState<@Composable () -> Unit>, + setFloatingButton: (@Composable () -> Unit) -> Unit, listState: LazyListState ) { val scope = rememberCoroutineScope() @@ -577,17 +581,18 @@ fun BoxWithConstraintsScope.FloatingButtons( LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible - floatingButton.value = bottomEndFloatingButton( - bottomUnreadCount, - showButtonWithCounter, - showButtonWithArrow, - onClickArrowDown = { - scope.launch { listState.animateScrollToItem(0) } - }, - onClickCounter = { - scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } - } - ) + setFloatingButton( + bottomEndFloatingButton( + bottomUnreadCount, + showButtonWithCounter, + showButtonWithArrow, + onClickArrowDown = { + scope.launch { listState.animateScrollToItem(0) } + }, + onClickCounter = { + scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } + } + )) } // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return @@ -735,7 +740,7 @@ private fun bottomEndFloatingButton( } } -private fun ViewConfiguration.bigTouchSlop(slop: Float = 80f) = object: ViewConfiguration { +private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConfiguration { override val longPressTimeoutMillis get() = this@bigTouchSlop.longPressTimeoutMillis From 9cd7a7fdb0a49430580caf682e5a94e4e22b1dc9 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 22 Aug 2022 16:02:46 +0300 Subject: [PATCH 46/89] Re-apply new chat instance on chatId changes (#962) * Re-apply new chat instance on chatId changes * Fixed incorrectly calculated counters on floating buttons * Show chat at the bottom of a view instead of at the top --- .../chat/simplex/app/views/chat/ChatView.kt | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 841c858732..095f3aec15 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -56,6 +56,22 @@ fun ChatView(chatModel: ChatModel) { val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. + // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .collect { + activeChat = if (chatModel.chatId.value == null) { + null + } else { + // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly + // Also for situation when chatId changes after clicking in notification, etc + chatModel.getChat(chatModel.chatId.value!!) + } + } + } + if (activeChat == null || user == null) { chatModel.chatId.value = null } else { @@ -121,11 +137,7 @@ fun ChatView(chatModel: ChatModel) { it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId } if (c != null) { - withApi { - openChat(c.chatInfo, chatModel) - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - activeChat = c - } + withApi { openChat(c.chatInfo, chatModel) } } }, loadPrevMessages = { cInfo -> @@ -262,7 +274,7 @@ fun ChatLayout( modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = { floatingButton.value() }, ) { contentPadding -> - BoxWithConstraints(Modifier.padding(contentPadding)) { + BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) { ChatItemsList( user, chat, unreadCount, composeState, chatItems, searchValue, useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, @@ -447,7 +459,7 @@ fun BoxWithConstraintsScope.ChatItemsList( Spacer(Modifier.size(8.dp)) val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } } - LazyColumn(state = listState, reverseLayout = true) { + LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { itemsIndexed(reversedChatItems) { i, cItem -> CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. @@ -558,6 +570,11 @@ fun BoxWithConstraintsScope.FloatingButtons( firstVisibleIndex = it firstItemIsVisible = firstVisibleIndex == 0 } + } + + LaunchedEffect(listState) { + // When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block + // so separate them into two LaunchedEffects snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } .distinctUntilChanged() .collect { From 6d1414af71ad623b400edcd0d06892270032a90f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:36:39 +0300 Subject: [PATCH 47/89] UI tweaks (#963) * UI tweaks * correction --- .../simplex/app/views/chat/ChatInfoView.kt | 14 +++++++++-- .../simplex/app/views/helpers/AlertManager.kt | 2 +- .../chat/simplex/app/views/helpers/Section.kt | 25 +++++++++++++++++++ .../app/src/main/res/values-ru/strings.xml | 3 ++- .../app/src/main/res/values/strings.xml | 1 + 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 7580abdce2..e214fdcda1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -1,10 +1,12 @@ package chat.simplex.app.views.chat import InfoRow +import InfoRowEllipsis import SectionDivider import SectionItemView import SectionSpacer import SectionView +import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -12,16 +14,20 @@ import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString 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.R +import chat.simplex.app.SimplexApp import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @@ -223,7 +229,11 @@ fun ServerImage(networkStatus: Chat.NetworkStatus) { @Composable fun SimplexServers(text: String, servers: List) { val info = servers.joinToString(separator = ", ") { it.substringAfter("@") } - InfoRow(text, info) + val clipboardManager: ClipboardManager = LocalClipboardManager.current + InfoRowEllipsis(text, info) { + clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ","))) + Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show() + } } @Composable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 7e22b00d2d..2adaf878e2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -79,7 +79,7 @@ class AlertManager { title = { Text(title) }, text = alertText, confirmButton = { - Button(onClick = { + TextButton(onClick = { onConfirm?.invoke() hideAlert() }) { Text(confirmText) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt index f77c179e57..6a04db96e3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt @@ -5,6 +5,8 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.app.ui.theme.GroupDark import chat.simplex.app.ui.theme.HighOrLowlight @@ -80,3 +82,26 @@ fun InfoRow(title: String, value: String) { } } } + +@Composable +fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { + SectionItemView { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val configuration = LocalConfiguration.current + Text(title) + Text(value, + Modifier + .padding(start = 10.dp) + .widthIn(max = (configuration.screenWidthDp / 2).dp) + .clickable(onClick = onClick), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = HighOrLowlight + ) + } + } +} diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 6aaeed14be..6a830e114a 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -160,6 +160,7 @@ нет описания Добавить контакт + Скопировано в буфер обмена Начать новый разговор @@ -273,7 +274,7 @@ Введите SMP серверы, каждый сервер в отдельной строке: Инфо Сохранить - Сеть & серверы + Сеть и серверы Настройки сети Настройки сети Использовать SOCKS прокси (порт 9050) diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 90d7fa256b..fd4f5a519b 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -160,6 +160,7 @@ Ok no details Add contact + Copied to clipboard Start new chat From a06499d710017ccbc37987c0171009399420aa70 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 22 Aug 2022 23:12:09 +0400 Subject: [PATCH 48/89] core: add host_conn_custom_user_profile_id to groups to replace join with connections causing duplicates (avoids complex subqueries) (#965) --- simplex-chat.cabal | 1 + ...groups_host_conn_custom_user_profile_id.hs | 12 +++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store.hs | 32 +++++++------------ 4 files changed, 26 insertions(+), 22 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index fe5cfc8b25..f1da990765 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -45,6 +45,7 @@ library Simplex.Chat.Migrations.M20220811_chat_items_indices Simplex.Chat.Migrations.M20220812_incognito_profiles Simplex.Chat.Migrations.M20220818_chat_notifications + Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs b/src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs new file mode 100644 index 0000000000..bbadbd5524 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220822_groups_host_conn_custom_user_profile_id :: Query +m20220822_groups_host_conn_custom_user_profile_id = + [sql| +ALTER TABLE groups ADD COLUMN host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- id of custom user profile used in direct connection with host +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f4f7ac3697..4807813c4b 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -121,7 +121,8 @@ CREATE TABLE groups( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, - enable_ntfs INTEGER, -- received + enable_ntfs INTEGER, + host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 3f28cb8e54..d323791d00 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -230,6 +230,7 @@ import Simplex.Chat.Migrations.M20220715_groups_chat_item_id import Simplex.Chat.Migrations.M20220811_chat_items_indices import Simplex.Chat.Migrations.M20220812_incognito_profiles import Simplex.Chat.Migrations.M20220818_chat_notifications +import Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -261,7 +262,8 @@ schemaMigrations = ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), ("20220811_chat_items_indices", m20220811_chat_items_indices), ("20220812_incognito_profiles", m20220812_incognito_profiles), - ("20220818_chat_notifications", m20220818_chat_notifications) + ("20220818_chat_notifications", m20220818_chat_notifications), + ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id) ] -- | The list of migrations in ascending order by date @@ -1179,7 +1181,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1193,8 +1195,6 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? |] @@ -1269,7 +1269,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1285,8 +1285,6 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) @@ -1357,8 +1355,8 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (profileId, localDisplayName, connRequest, userId, True, currentTs, currentTs) + "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs) insertedRowId db _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs @@ -1474,15 +1472,13 @@ getUserGroupDetails db User {userId, userContactId} = <$> DB.query db [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id WHERE g.user_id = ? AND mu.contact_id = ? |] (userId, userContactId) @@ -1898,7 +1894,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1915,8 +1911,6 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) @@ -2764,7 +2758,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -2788,8 +2782,6 @@ getGroupChatPreviews_ db User {userId, userContactId} = do FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) LEFT JOIN ( SELECT group_id, MAX(chat_item_id) AS MaxId @@ -3166,7 +3158,7 @@ getGroupInfo db User {userId, userContactId} groupId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, hc.custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -3174,8 +3166,6 @@ getGroupInfo db User {userId, userContactId} groupId = FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id - LEFT JOIN contacts h ON h.contact_id = mu.invited_by - LEFT JOIN connections hc ON hc.contact_id = h.contact_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? |] From 04592f52deb662203269887ebe00b8273b34b00f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 22 Aug 2022 23:16:01 +0300 Subject: [PATCH 49/89] Ability to disable notifications per chat (#964) * Ability to disable notifications per chat * All Buttons in AlertDialog replaced with TextButtons * update icon * update strings Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 19 ++++- .../java/chat/simplex/app/model/NtfManager.kt | 2 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 17 +++- .../simplex/app/views/chat/ChatInfoView.kt | 81 ++++++++++++++++++- .../app/views/chat/group/GroupChatInfoView.kt | 22 ++++- .../app/views/chat/item/ChatItemView.kt | 4 +- .../app/views/chatlist/ChatListNavLinkView.kt | 18 ++++- .../app/views/chatlist/ChatPreviewView.kt | 24 +++++- .../simplex/app/views/helpers/AlertManager.kt | 4 +- .../app/src/main/res/values-ru/strings.xml | 7 ++ .../app/src/main/res/values/strings.xml | 7 ++ 11 files changed, 183 insertions(+), 22 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index cc4f7046ef..e83f365cdd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -325,6 +325,7 @@ interface SomeChat { val apiId: Long val ready: Boolean val sendMsgEnabled: Boolean + val ntfsEnabled: Boolean val createdAt: Instant val updatedAt: Instant } @@ -383,6 +384,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val sendMsgEnabled get() = contact.sendMsgEnabled + override val ntfsEnabled get() = contact.chatSettings.enableNtfs override val createdAt get() = contact.createdAt override val updatedAt get() = contact.updatedAt override val displayName get() = contact.displayName @@ -402,6 +404,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready override val sendMsgEnabled get() = groupInfo.sendMsgEnabled + override val ntfsEnabled get() = groupInfo.chatSettings.enableNtfs override val createdAt get() = groupInfo.createdAt override val updatedAt get() = groupInfo.updatedAt override val displayName get() = groupInfo.displayName @@ -421,6 +424,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val sendMsgEnabled get() = contactRequest.sendMsgEnabled + override val ntfsEnabled get() = false override val createdAt get() = contactRequest.createdAt override val updatedAt get() = contactRequest.updatedAt override val displayName get() = contactRequest.displayName @@ -440,6 +444,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val sendMsgEnabled get() = contactConnection.sendMsgEnabled + override val ntfsEnabled get() = false override val createdAt get() = contactConnection.createdAt override val updatedAt get() = contactConnection.updatedAt override val displayName get() = contactConnection.displayName @@ -454,13 +459,13 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable -class Contact( +data class Contact( val contactId: Long, override val localDisplayName: String, val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, -// val chatSettings: ChatSettings, + val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -469,6 +474,7 @@ class Contact( override val apiId get() = contactId override val ready get() = activeConn.connStatus == ConnStatus.Ready override val sendMsgEnabled get() = true + override val ntfsEnabled get() = chatSettings.enableNtfs override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image @@ -482,6 +488,7 @@ class Contact( localDisplayName = "alice", profile = Profile.sampleData, activeConn = Connection.sampleData, + chatSettings = ChatSettings(true), createdAt = Clock.System.now(), updatedAt = Clock.System.now() ) @@ -536,12 +543,12 @@ class Group ( ) @Serializable -class GroupInfo ( +data class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, val membership: GroupMember, - // val chatSettings: ChatSettings, + val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -550,6 +557,7 @@ class GroupInfo ( override val apiId get() = groupId override val ready get() = true override val sendMsgEnabled get() = membership.memberActive + override val ntfsEnabled get() = chatSettings.enableNtfs override val displayName get() = groupProfile.displayName override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image @@ -569,6 +577,7 @@ class GroupInfo ( localDisplayName = "team", groupProfile = GroupProfile.sampleData, membership = GroupMember.sampleData, + chatSettings = ChatSettings(true), createdAt = Clock.System.now(), updatedAt = Clock.System.now() ) @@ -770,6 +779,7 @@ class UserContactRequest ( override val apiId get() = contactRequestId override val ready get() = true override val sendMsgEnabled get() = false + override val ntfsEnabled get() = false override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image @@ -799,6 +809,7 @@ class PendingContactConnection( override val apiId get() = pccConnId override val ready get() = false override val sendMsgEnabled get() = false + override val ntfsEnabled get() = false override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId) override val displayName: String get() { val initiated = pccConnStatus.initiated diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index c64b1c8d24..c854183a70 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -64,6 +64,8 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference } fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { + if (!cInfo.ntfsEnabled) return + notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a9684da5a6..2166882959 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -388,6 +388,17 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } + suspend fun apiSetSettings(type: ChatType,id: Long, settings: ChatSettings): Boolean { + val r = sendCmd(CC.APISetChatSettings(type, id, settings)) + return when (r) { + is CR.CmdOk -> true + else -> { + Log.e(TAG, "apiSetSettings bad response: ${r.responseType} ${r.details}") + false + } + } + } + suspend fun apiContactInfo(contactId: Long): ConnectionStats? { val r = sendCmd(CC.APIContactInfo(contactId)) if (r is CR.ContactInfo) return r.connectionStats @@ -942,7 +953,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } @@ -973,7 +984,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) } + TextButton(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) } } ) } @@ -999,7 +1010,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index e214fdcda1..72c656261c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -43,11 +43,43 @@ fun ChatInfoView(chatModel: ChatModel, connStats: ConnectionStats?, close: () -> connStats, developerTools, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, - clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) } + clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, + changeNtfsState = { enabled -> + changeNtfsState(enabled, chat, chatModel) + }, ) } } +fun changeNtfsState(enabled: Boolean, chat: Chat, chatModel: ChatModel) { + val newChatInfo = when(chat.chatInfo) { + is ChatInfo.Direct -> with (chat.chatInfo) { + ChatInfo.Direct(contact.copy(chatSettings = contact.chatSettings.copy(enableNtfs = enabled))) + } + is ChatInfo.Group -> with(chat.chatInfo) { + ChatInfo.Group(groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(enableNtfs = enabled))) + } + else -> null + } + withApi { + val res = when (newChatInfo) { + is ChatInfo.Direct -> with(newChatInfo) { + chatModel.controller.apiSetSettings(chatType, apiId, contact.chatSettings) + } + is ChatInfo.Group -> with(newChatInfo) { + chatModel.controller.apiSetSettings(chatType, apiId, groupInfo.chatSettings) + } + else -> false + } + if (res && newChatInfo != null) { + chatModel.updateChatInfo(newChatInfo) + if (!enabled) { + chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + } + } + } +} + fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.delete_contact_question), @@ -91,7 +123,8 @@ fun ChatInfoLayout( connStats: ConnectionStats?, developerTools: Boolean, deleteContact: () -> Unit, - clearChat: () -> Unit + clearChat: () -> Unit, + changeNtfsState: (Boolean) -> Unit, ) { Column( Modifier @@ -126,6 +159,17 @@ fun ChatInfoLayout( SectionSpacer() } + var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + SectionView(title = stringResource(R.string.settings_section_title_settings)) { + SectionItemView { + NtfsSwitch(ntfsEnabled) { + ntfsEnabled = !ntfsEnabled + changeNtfsState(ntfsEnabled) + } + } + } + SectionSpacer() + SectionView { SectionItemView { ClearChatButton(clearChat) @@ -236,6 +280,38 @@ fun SimplexServers(text: String, servers: List) { } } +@Composable +fun NtfsSwitch( + ntfsEnabled: Boolean, + toggleNtfs: (Boolean) -> Unit +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Outlined.Notifications, + stringResource(R.string.notifications), + tint = HighOrLowlight + ) + Text(stringResource(R.string.notifications)) + } + Switch( + checked = ntfsEnabled, + onCheckedChange = toggleNtfs, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + ) + } +} + @Composable fun ClearChatButton(clearChat: () -> Unit) { Row( @@ -282,6 +358,7 @@ fun PreviewChatInfoLayout() { chatItems = arrayListOf(), serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) ), + changeNtfsState = {}, developerTools = false, connStats = null, deleteContact = {}, clearChat = {} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 4ebc59d5b9..d2d7787718 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -72,7 +72,10 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { }, deleteGroup = { deleteGroupDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, - leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) } + leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, + changeNtfsState = { enabled -> + changeNtfsState(enabled, chat, chatModel) + }, ) } } @@ -122,6 +125,7 @@ fun GroupChatInfoLayout( deleteGroup: () -> Unit, clearChat: () -> Unit, leaveGroup: () -> Unit, + changeNtfsState: (Boolean) -> Unit, ) { Column( Modifier @@ -154,6 +158,17 @@ fun GroupChatInfoLayout( } SectionSpacer() + var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + SectionView(title = stringResource(R.string.settings_section_title_settings)) { + SectionItemView { + NtfsSwitch(ntfsEnabled) { + ntfsEnabled = !ntfsEnabled + changeNtfsState(ntfsEnabled) + } + } + } + SectionSpacer() + SectionView { if (groupInfo.canEdit) { SectionItemView { @@ -322,7 +337,8 @@ fun PreviewGroupChatInfoLayout() { groupInfo = GroupInfo.sampleData, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {} + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, + changeNtfsState = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 7392526c39..9c8f05238a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -184,13 +184,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteM .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = Arrangement.End, ) { - Button(onClick = { + TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.for_me_only)) } if (chatItem.meta.editable) { Spacer(Modifier.padding(horizontal = 4.dp)) - Button(onClick = { + TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.for_everybody)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index ade4c9abe8..fb9faf5457 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -119,6 +119,7 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + ItemAction( + if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat), + if (ntfsEnabled) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications, + onClick = { + changeNtfsState(!ntfsEnabled, chat, chatModel) + showMenu.value = false + } + ) +} + @Composable fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { ItemAction( @@ -318,14 +332,14 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = Arrangement.End, ) { - Button(onClick = { + TextButton(onClick = { AlertManager.shared.hideAlert() deleteContactConnectionAlert(connection, chatModel) }) { Text(stringResource(R.string.delete_verb)) } Spacer(Modifier.padding(horizontal = 4.dp)) - Button(onClick = { AlertManager.shared.hideAlert() }) { + TextButton(onClick = { AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.ok)) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 65161deeec..5bb8e8526f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -8,7 +8,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,8 +24,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.ChatInfoImage -import chat.simplex.app.views.helpers.badgeLayout +import chat.simplex.app.views.helpers.* @Composable fun ChatPreviewView(chat: Chat, stopped: Boolean) { @@ -134,6 +134,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { modifier = Modifier.padding(bottom = 5.dp) ) val n = chat.chatStats.unreadCount + val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) if (n > 0) { Box( Modifier.padding(top = 24.dp), @@ -144,12 +145,27 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { color = MaterialTheme.colors.onPrimary, fontSize = 11.sp, modifier = Modifier - .background(if (stopped) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) + .background(if (stopped || showNtfsIcon) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) .badgeLayout() .padding(horizontal = 3.dp) .padding(vertical = 1.dp) ) } + } else if (showNtfsIcon) { + Box( + Modifier.padding(top = 24.dp), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Filled.NotificationsOff, + contentDescription = generalGetString(R.string.notifications), + tint = HighOrLowlight, + modifier = Modifier + .padding(horizontal = 3.dp) + .padding(vertical = 1.dp) + .size(17.dp) + ) + } } if (cInfo is ChatInfo.Direct) { Box( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 2adaf878e2..55715f8e84 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -53,13 +53,13 @@ class AlertManager { title = { Text(title) }, text = alertText, confirmButton = { - Button(onClick = { + TextButton(onClick = { onConfirm?.invoke() hideAlert() }) { Text(confirmText) } }, dismissButton = { - Button(onClick = { + TextButton(onClick = { onDismiss?.invoke() hideAlert() }) { Text(dismissText) } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 6a830e114a..b8246adb74 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -141,6 +141,9 @@ Файл не найден Ошибка сохранения файла + + Уведомления + Удалить контакт? Контакт и все сообщения будут удалены - это действие нельзя отменить! @@ -205,6 +208,10 @@ Очистить чат Прочитано + + Без звука + Уведомлять + Вы пригласили ваш контакт Вы приняли приглашение соединиться diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index fd4f5a519b..ca4aeb42e9 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -141,6 +141,9 @@ File not found Error saving file + + Notifications + Delete contact? Contact and all messages will be deleted - this cannot be undone! @@ -205,6 +208,10 @@ Clear chat Mark read + + Mute + Unmute + You invited your contact You accepted connection From bd7aa81625c664880f86c789e09178f0250c9b24 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 23 Aug 2022 13:24:43 +0100 Subject: [PATCH 50/89] core: DB json encoding for group events (platform independent) (#966) * core: DB json encoding for group events (platform independent) * comment, migration * shorter constructors Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- simplex-chat.cabal | 1 + src/Simplex/Chat/Messages.hs | 35 +++++++++++++++---- ...23_delete_broken_group_event_chat_items.hs | 13 +++++++ src/Simplex/Chat/Store.hs | 4 ++- 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f1da990765..63dba222a4 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -46,6 +46,7 @@ library Simplex.Chat.Migrations.M20220812_incognito_profiles Simplex.Chat.Migrations.M20220818_chat_notifications Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id + Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 02fa60d937..8633d25207 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -521,6 +521,8 @@ profileToText :: Profile -> Text profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName -- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API +-- ! Nested sum types also have to use different encodings for database and API +-- ! to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent data CIContent (d :: MsgDirection) where CISndMsgContent :: MsgContent -> CIContent 'MDSnd CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv @@ -533,6 +535,9 @@ data CIContent (d :: MsgDirection) where CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd +-- ^^^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API +-- ! ^^^ Nested sum types also have to use different encodings for database and API +-- ! ^^^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent deriving instance Show (CIContent d) @@ -553,6 +558,15 @@ instance ToJSON RcvGroupEvent where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RGE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RGE" +newtype DBRcvGroupEvent = RGE RcvGroupEvent + +instance FromJSON DBRcvGroupEvent where + parseJSON v = RGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RGE") v + +instance ToJSON DBRcvGroupEvent where + toJSON (RGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RGE") v + toEncoding (RGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RGE") v + data SndGroupEvent = SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember | SGEUserLeft -- CRLeftMemberUser @@ -566,6 +580,15 @@ instance ToJSON SndGroupEvent where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SGE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SGE" +newtype DBSndGroupEvent = SGE SndGroupEvent + +instance FromJSON DBSndGroupEvent where + parseJSON v = SGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SGE") v + +instance ToJSON DBSndGroupEvent where + toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v + toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v + data CIGroupInvitation = CIGroupInvitation { groupId :: GroupId, groupMemberId :: GroupMemberId, @@ -703,8 +726,8 @@ data DBJSONCIContent | DBJCIRcvIntegrityError {msgError :: MsgErrorType} | DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} - | DBJCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent} - | DBJCISndGroupEvent {sndGroupEvent :: SndGroupEvent} + | DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent} + | DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent} deriving (Generic) instance FromJSON DBJSONCIContent where @@ -725,8 +748,8 @@ dbJsonCIContent = \case CIRcvIntegrityError err -> DBJCIRcvIntegrityError err CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole} CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole} - CIRcvGroupEvent rcvGroupEvent -> DBJCIRcvGroupEvent {rcvGroupEvent} - CISndGroupEvent sndGroupEvent -> DBJCISndGroupEvent {sndGroupEvent} + CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge + CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge aciContentDBJSON :: DBJSONCIContent -> ACIContent aciContentDBJSON = \case @@ -739,8 +762,8 @@ aciContentDBJSON = \case DBJCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole - DBJCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent - DBJCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent + DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge + DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge data CICallStatus = CISCallPending diff --git a/src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs b/src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs new file mode 100644 index 0000000000..40657f3421 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220823_delete_broken_group_event_chat_items :: Query +m20220823_delete_broken_group_event_chat_items = + [sql| +DELETE FROM chat_items WHERE item_content LIKE '%{"rcvGroupEvent":{"rcvGroupEvent":{%'; +DELETE FROM chat_items WHERE item_content LIKE '%{"sndGroupEvent":{"sndGroupEvent":{%'; +|] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index d323791d00..3b3520437b 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -231,6 +231,7 @@ import Simplex.Chat.Migrations.M20220811_chat_items_indices import Simplex.Chat.Migrations.M20220812_incognito_profiles import Simplex.Chat.Migrations.M20220818_chat_notifications import Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id +import Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -263,7 +264,8 @@ schemaMigrations = ("20220811_chat_items_indices", m20220811_chat_items_indices), ("20220812_incognito_profiles", m20220812_incognito_profiles), ("20220818_chat_notifications", m20220818_chat_notifications), - ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id) + ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id), + ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items) ] -- | The list of migrations in ascending order by date From e6551abc68b4368fe4cfbf16612b3f3ee04e3526 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 23 Aug 2022 18:18:12 +0400 Subject: [PATCH 51/89] ios: incognito mode (#945) * ios: incognito types * wip * wip * wip * wip * wip * cleaner interface * CIGroupInvitationView logic * masks not filled * ui improvements * wip * wip * incognito may be compromised alerts * help * remove modifier * Update apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * contact request * texts * ; * prepare for merge * restore help * wip * update help * wip * update incognito help * the * Update apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * wording * translations * secondary color * translations * translations * fix Your Chats title Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/ChatModel.swift | 3 +- apps/ios/Shared/Model/SimpleXAPI.swift | 15 +- .../Shared/Views/Chat/ChatInfoToolbar.swift | 4 + apps/ios/Shared/Views/Chat/ChatInfoView.swift | 7 + .../Chat/ChatItem/CIGroupInvitationView.swift | 44 ++- apps/ios/Shared/Views/Chat/ChatItemView.swift | 2 +- apps/ios/Shared/Views/Chat/ChatView.swift | 10 +- .../Chat/Group/AddGroupMembersView.swift | 118 +++++-- .../Views/Chat/Group/GroupChatInfoView.swift | 12 +- .../Chat/Group/GroupMemberInfoView.swift | 18 + .../Views/ChatList/ChatListNavLink.swift | 103 ++++-- .../Shared/Views/ChatList/ChatListView.swift | 11 + .../Views/ChatList/ChatPreviewView.swift | 10 +- .../Views/ChatList/ContactRequestView.swift | 4 +- .../Shared/Views/NewChat/AddContactView.swift | 18 +- .../Shared/Views/NewChat/AddGroupView.swift | 14 + .../Views/NewChat/PasteToConnectView.swift | 19 +- .../Views/NewChat/ScanToConnectView.swift | 16 +- .../Views/UserSettings/CallSettings.swift | 10 +- .../Views/UserSettings/IncognitoHelp.swift | 57 ++++ .../Views/UserSettings/SettingsView.swift | 45 ++- .../Views/UserSettings/UserProfile.swift | 8 +- .../en.xcloc/Localized Contents/en.xliff | 321 +++++++++++++++++- .../ru.xcloc/Localized Contents/ru.xliff | 321 +++++++++++++++++- .../ios/SimpleX NSE/NotificationService.swift | 7 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 44 +-- apps/ios/SimpleXChat/APITypes.swift | 11 +- apps/ios/SimpleXChat/AppGroup.swift | 6 +- apps/ios/SimpleXChat/ChatTypes.swift | 117 ++++++- apps/ios/ru.lproj/Localizable.strings | 197 ++++++++++- src/Simplex/Chat/Types.hs | 3 - 31 files changed, 1392 insertions(+), 183 deletions(-) create mode 100644 apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 046d18ce59..6949762925 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -36,6 +36,7 @@ final class ChatModel: ObservableObject { @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off @Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get() + @Published var incognito: Bool = incognitoGroupDefault.get() // pending notification actions @Published var ntfContactRequest: ChatId? @Published var ntfCallInvitationAction: (ChatId, NtfCallAction)? @@ -79,7 +80,7 @@ final class ChatModel: ObservableObject { } func updateContact(_ contact: Contact) { - updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact()) + updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact) } func updateGroup(_ groupInfo: GroupInfo) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 84f8699074..6651b577b2 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -167,6 +167,12 @@ func apiSetFilesFolder(filesFolder: String) throws { throw r } +func apiSetIncognito(incognito: Bool) throws { + let r = chatSendCmdSync(.setIncognito(incognito: incognito)) + if case .cmdOk = r { return } + throw r +} + func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -312,15 +318,15 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings)) } -func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? { +func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile?) { let r = await chatSendCmd(.apiContactInfo(contactId: contactId)) - if case let .contactInfo(_, connStats) = r { return connStats } + if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) } throw r } -func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> ConnectionStats? { +func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?, Profile?) { let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ } + if case let .groupMemberInfo(_, _, connStats_, mainProfile) = r { return (connStats_, mainProfile) } throw r } @@ -646,6 +652,7 @@ func initializeChat(start: Bool) throws { do { let m = ChatModel.shared try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetIncognito(incognito: incognitoGroupDefault.get()) m.currentUser = try apiGetActiveUser() if m.currentUser == nil { m.onboardingStage = .step1_SimpleXInfo diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index d57e0b9925..7e527aef44 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -19,6 +19,10 @@ struct ChatInfoToolbar: View { var body: some View { let cInfo = chat.chatInfo return HStack { + if (cInfo.incognito) { + Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo) + Spacer().frame(width: 16) + } ChatInfoImage( chat: chat, color: colorScheme == .dark diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 253ccb0f15..0450f79816 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -47,6 +47,7 @@ struct ChatInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat var connectionStats: ConnectionStats? + var customUserProfile: Profile? @State private var alert: ChatInfoViewAlert? = nil @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -64,6 +65,12 @@ struct ChatInfoView: View { contactInfoHeader() .listRowBackground(Color.clear) + if let customUserProfile = customUserProfile { + Section("Incognito") { + infoRow("Your random profile", customUserProfile.chatViewName) + } + } + if let connStats = connectionStats { Section("Servers") { networkStatusRow() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 4419a884d4..8fc5421f52 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -10,14 +10,17 @@ import SwiftUI import SimpleXChat struct CIGroupInvitationView: View { + @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme var chatItem: ChatItem var groupInvitation: CIGroupInvitation var memberRole: GroupMemberRole + var chatIncognito: Bool = false @State private var frameWidth: CGFloat = 0 var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending + let unsafeToJoinIncognito = interactiveIncognito && !chatIncognito let v = ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading) { groupInfoView(action) @@ -29,10 +32,13 @@ struct CIGroupInvitationView: View { Divider().frame(width: frameWidth) if action { - groupInvitationText().overlay(DetermineWidth()) - Text("Tap to join") - .foregroundColor(.accentColor) + groupInvitationText() + .overlay(DetermineWidth()) + Text(interactiveIncognito ? "Tap to join incognito" : "Tap to join") + .foregroundColor(interactiveIncognito ? .indigo : .accentColor) .font(.callout) + .padding(.trailing, 60) + .overlay(DetermineWidth()) } else { groupInvitationText() .padding(.trailing, 60) @@ -52,17 +58,34 @@ struct CIGroupInvitationView: View { .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } if action { - v.onTapGesture { acceptInvitation() } + v.onTapGesture { + if unsafeToJoinIncognito { + AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(groupInvitation.groupId)) + } else { + joinGroup(groupInvitation.groupId) + } + } } else { v } } + private var interactiveIncognito: Bool { + (groupInvitation.invitedIncognito ?? false) || chatModel.incognito + } + private func groupInfoView(_ action: Bool) -> some View { - HStack(alignment: .top) { + var color: Color + if action { + color = interactiveIncognito ? .indigo : .accentColor + } else { + color = Color(uiColor: .tertiaryLabel) + } + return HStack(alignment: .top) { ProfileImage( + imageStr: groupInvitation.groupProfile.image, iconName: "person.2.circle.fill", - color: action ? .accentColor : Color(uiColor: .tertiaryLabel) + color: color ) .frame(width: 44, height: 44) .padding(.trailing, 4) @@ -84,7 +107,7 @@ struct CIGroupInvitationView: View { private func groupInvitationStr() -> LocalizedStringKey { if chatItem.chatDir.sent { - return "You sent group invitation" + return (groupInvitation.invitedIncognito ?? false) ? "You sent group invitation incognito" : "You sent group invitation" } else { switch groupInvitation.status { case .pending: return "You are invited to group" @@ -94,13 +117,6 @@ struct CIGroupInvitationView: View { } } } - - private func acceptInvitation() { - Task { - logger.debug("acceptInvitation") - await joinGroup(groupInvitation.groupId) - } - } } struct CIGroupInvitationView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index c43fc9daf0..af71f74307 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -48,7 +48,7 @@ struct ChatItemView: View { } private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View { - CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole) + CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito) } private func groupEventItemView() -> some View { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 5bfd2d98b6..049637f2d1 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -22,6 +22,7 @@ struct ChatView: View { @FocusState private var keyboardVisible: Bool @State private var showDeleteMessage = false @State private var connectionStats: ConnectionStats? + @State private var customUserProfile: Profile? @State private var tableView: UITableView? @State private var loadingItems = false @State private var firstPage = false @@ -76,8 +77,11 @@ struct ChatView: View { if case .direct = cInfo { Task { do { - let stats = try await apiContactInfo(contactId: chat.chatInfo.apiId) - await MainActor.run { connectionStats = stats } + let (stats, profile) = try await apiContactInfo(contactId: chat.chatInfo.apiId) + await MainActor.run { + connectionStats = stats + customUserProfile = profile + } } catch let error { logger.error("apiContactInfo error: \(responseError(error))") } @@ -98,7 +102,7 @@ struct ChatView: View { .sheet(isPresented: $showChatInfoSheet) { switch cInfo { case .direct: - ChatInfoView(chat: chat, connectionStats: connectionStats) + ChatInfoView(chat: chat, connectionStats: connectionStats, customUserProfile: customUserProfile) case let .group(groupInfo): GroupChatInfoView(chat: chat, groupInfo: groupInfo) default: diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index d001d9ff0c..6a75758e02 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -18,10 +18,29 @@ struct AddGroupMembersView: View { var addedMembersCb: ((Set) -> Void)? = nil @State private var selectedContacts = Set() @State private var selectedRole: GroupMemberRole = .admin + @State private var alert: AddGroupMembersAlert? + + private enum AddGroupMembersAlert: Identifiable { + case prohibitedToInviteIncognito + case warnUnsafeToInviteIncognito + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .prohibitedToInviteIncognito: return "prohibitedToInviteIncognito" + case .warnUnsafeToInviteIncognito: return "warnUnsafeToInviteIncognito" + case let .error(title, _): return "error \(title)" + } + } + } var body: some View { NavigationView { let membersToAdd = filterMembersToAdd(chatModel.groupMembers) + let nonIncognitoConnectionsSelected = membersToAdd + .filter{ selectedContacts.contains($0.apiId) } + .contains(where: { !$0.contactConnIncognito }) + let unsafeToInviteIncognito = chat.chatInfo.incognito && nonIncognitoConnectionsSelected let v = List { ChatInfoToolbar(chat: chat, imageSize: 48) @@ -39,7 +58,7 @@ struct AddGroupMembersView: View { let count = selectedContacts.count Section { rolePicker() - inviteMembersButton() + inviteMembersButton(unsafeToInviteIncognito) .disabled(count < 1) } footer: { if (count >= 1) { @@ -77,26 +96,33 @@ struct AddGroupMembersView: View { } } .frame(maxHeight: .infinity, alignment: .top) + .alert(item: $alert) { alert in + switch alert { + case .prohibitedToInviteIncognito: + return Alert( + title: Text("Can't invite contact!"), + message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile") + ) + case .warnUnsafeToInviteIncognito: + return Alert( + title: Text("Your main profile may be shared"), + message: Text("Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."), + primaryButton: .destructive(Text("Invite anyway")) { + inviteMembers() + }, secondaryButton: .cancel() + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text("\(error)")) + } + } } - func inviteMembersButton() -> some View { + private func inviteMembersButton(_ unsafeToInviteIncognito: Bool) -> some View { Button { - Task { - do { - for contactId in selectedContacts { - let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole) - await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) } - } - await MainActor.run { dismiss() } - if let cb = addedMembersCb { cb(selectedContacts) } - } catch { - AlertManager.shared.showAlert( - Alert( - title: Text("Error adding member(s)"), - message: Text(responseError(error)) - ) - ) - } + if unsafeToInviteIncognito { + alert = .warnUnsafeToInviteIncognito + } else { + inviteMembers() } } label: { HStack { @@ -107,7 +133,22 @@ struct AddGroupMembersView: View { .frame(maxWidth: .infinity, alignment: .trailing) } - func rolePicker() -> some View { + private func inviteMembers() { + Task { + do { + for contactId in selectedContacts { + let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole) + await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) } + } + await MainActor.run { dismiss() } + if let cb = addedMembersCb { cb(selectedContacts) } + } catch { + alert = .error(title: "Error adding member(s)", error: responseError(error)) + } + } + } + + private func rolePicker() -> some View { Picker("New member role", selection: $selectedRole) { ForEach(GroupMemberRole.allCases) { role in if role <= groupInfo.membership.memberRole { @@ -117,13 +158,33 @@ struct AddGroupMembersView: View { } } - func contactCheckView(_ contact: Contact) -> some View { + private func contactCheckView(_ contact: Contact) -> some View { let checked = selectedContacts.contains(contact.apiId) - return Button { + let prohibitedToInviteIncognito = !chat.chatInfo.incognito && contact.contactConnIncognito + let safeToInviteIncognito = chat.chatInfo.incognito && contact.contactConnIncognito + var icon: String + var iconColor: Color + if prohibitedToInviteIncognito { + icon = "theatermasks.circle.fill" + iconColor = Color(uiColor: .tertiaryLabel) + } else { if checked { - selectedContacts.remove(contact.apiId) + icon = "checkmark.circle.fill" + iconColor = .accentColor } else { - selectedContacts.insert(contact.apiId) + icon = "circle" + iconColor = Color(uiColor: .tertiaryLabel) + } + } + return Button { + if prohibitedToInviteIncognito { + alert = .prohibitedToInviteIncognito + } else { + if checked { + selectedContacts.remove(contact.apiId) + } else { + selectedContacts.insert(contact.apiId) + } } } label: { HStack{ @@ -131,11 +192,16 @@ struct AddGroupMembersView: View { .frame(width: 30, height: 30) .padding(.trailing, 2) Text(ChatInfo.direct(contact: contact).chatViewName) - .foregroundColor(.primary) + .foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary) .lineLimit(1) Spacer() - Image(systemName: checked ? "checkmark.circle.fill": "circle") - .foregroundColor(checked ? .accentColor : Color(uiColor: .tertiaryLabel)) + if safeToInviteIncognito { + Image(systemName: "theatermasks") + .foregroundColor(.indigo) + .font(.footnote) + } + Image(systemName: icon) + .foregroundColor(iconColor) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 7e6b2edb9a..68f402c0eb 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -20,6 +20,7 @@ struct GroupChatInfoView: View { @State private var selectedMember: GroupMember? = nil @State private var showGroupProfile: Bool = false @State private var connectionStats: ConnectionStats? + @State private var mainProfile: Profile? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum GroupChatInfoViewAlert: Identifiable { @@ -49,8 +50,11 @@ struct GroupChatInfoView: View { Button { Task { do { - let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - await MainActor.run { connectionStats = stats } + let (stats, profile) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + await MainActor.run { + connectionStats = stats + mainProfile = profile + } } catch let error { logger.error("apiGroupMemberInfo error: \(responseError(error))") } @@ -63,7 +67,7 @@ struct GroupChatInfoView: View { AddGroupMembersView(chat: chat, groupInfo: groupInfo) } .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats) + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: mainProfile) } .sheet(isPresented: $showGroupProfile) { GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) @@ -150,7 +154,7 @@ struct GroupChatInfoView: View { VStack(alignment: .leading) { Text(member.chatViewName) .lineLimit(1) - .foregroundColor(.primary) + .foregroundColor(member.memberIncognito ? .indigo : .primary) let s = Text(member.memberStatus.shortText) (user ? Text ("you: ") + s : s) .lineLimit(1) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index a34f5dc72d..3c0b257b21 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -15,6 +15,7 @@ struct GroupMemberInfoView: View { var groupInfo: GroupInfo var member: GroupMember var connectionStats: ConnectionStats? + var mainProfile: Profile? @State private var alert: GroupMemberInfoViewAlert? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -32,6 +33,9 @@ struct GroupMemberInfoView: View { Section("Member") { infoRow("Group", groupInfo.displayName) + if let mainProfile = mainProfile { + mainProfileRow(mainProfile) + } // TODO change role // localizedInfoRow("Role", member.memberRole.text) // TODO invited by - need to get contact by contact id @@ -72,6 +76,20 @@ struct GroupMemberInfoView: View { } } + private func mainProfileRow(_ mainProfile: Profile) -> some View { + HStack { + Text("Known main profile") + Spacer() + if (mainProfile.image != nil) { + ProfileImage(imageStr: member.image) + .frame(width: 38, height: 38) + .padding(.trailing, 2) + } + Text(mainProfile.chatViewName) + .foregroundColor(.secondary) + } + } + private func groupMemberInfoHeader() -> some View { VStack { ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill)) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 74756bfabd..416a7ca82d 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -75,14 +75,20 @@ struct ChatListNavLink: View { ChatPreviewView(chat: chat) .frame(height: 80) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - joinGroupButton() + joinGroupButton(groupInfo.hostConnCustomUserProfileId) if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } } .onTapGesture { showJoinGroupDialog = true } .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { - Button("Join group") { Task { await joinGroup(groupInfo.groupId) } } + Button(interactiveIncognito ? "Join incognito" : "Join group") { + if unsafeToJoinIncognito(groupInfo.hostConnCustomUserProfileId) { + AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId)) + } else { + joinGroup(groupInfo.groupId) + } + } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } } case .memAccepted: @@ -113,7 +119,7 @@ struct ChatListNavLink: View { } label: { Label("Leave", systemImage: "rectangle.portrait.and.arrow.right") } - .tint(Color.indigo) + .tint(Color.yellow) } } .swipeActions(edge: .trailing) { @@ -124,13 +130,25 @@ struct ChatListNavLink: View { } } - private func joinGroupButton() -> some View { + private var interactiveIncognito: Bool { + chat.chatInfo.incognito || chatModel.incognito + } + + private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View { Button { - Task { await joinGroup(chat.chatInfo.apiId) } + if unsafeToJoinIncognito(hostConnCustomUserProfileId) { + AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId)) + } else { + joinGroup(chat.chatInfo.apiId) + } } label: { - Label("Join", systemImage: "ipad.and.arrow.forward") + Label("Join", systemImage: interactiveIncognito ? "theatermasks" : "ipad.and.arrow.forward") } - .tint(Color.accentColor) + .tint(interactiveIncognito ? .indigo : .accentColor) + } + + private func unsafeToJoinIncognito(_ hostConnCustomUserProfileId: Int64?) -> Bool { + interactiveIncognito && hostConnCustomUserProfileId == nil } private func markReadButton() -> some View { @@ -162,9 +180,10 @@ struct ChatListNavLink: View { private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { ContactRequestView(contactRequest: contactRequest, chat: chat) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button { Task { await acceptContactRequest(contactRequest) } } - label: { Label("Accept", systemImage: "checkmark") } - .tint(Color.accentColor) + Button { + Task { await acceptContactRequest(contactRequest) } + } label: { Label("Accept", systemImage: chatModel.incognito ? "theatermasks" : "checkmark") } + .tint(chatModel.incognito ? .indigo : .accentColor) Button(role: .destructive) { AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) } label: { @@ -174,7 +193,7 @@ struct ChatListNavLink: View { .frame(height: 80) .onTapGesture { showContactRequestDialog = true } .confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) { - Button("Accept contact") { Task { await acceptContactRequest(contactRequest) } } + Button(chatModel.incognito ? "Accept incognito" : "Accept contact") { Task { await acceptContactRequest(contactRequest) } } Button("Reject contact (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } } } } @@ -325,32 +344,46 @@ struct ChatListNavLink: View { } } -func joinGroup(_ groupId: Int64) async { - do { - let r = try await apiJoinGroup(groupId) - switch r { - case let .joined(groupInfo): - await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } - case .invitationRemoved: - AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") - await deleteGroup() - case .groupNotFound: - AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") - await deleteGroup() - } - } catch let error { - let err = responseError(error) - AlertManager.shared.showAlert(Alert(title: Text("Error joining group"), message: Text(err))) - logger.error("apiJoinGroup error: \(err)") - } +func unsafeToJoinIncognitoAlert(_ groupId: Int64) -> Alert { + Alert( + title: Text("Your main profile may be shared"), + message: Text("The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."), + primaryButton: .destructive(Text("Join anyway")) { + joinGroup(groupId) + }, + secondaryButton: .cancel() + ) +} - func deleteGroup() async { +func joinGroup(_ groupId: Int64) { + Task { + logger.debug("joinGroup") do { - // TODO this API should update chat item with the invitation as well - try await apiDeleteChat(type: .group, id: groupId) - await MainActor.run { ChatModel.shared.removeChat("#\(groupId)") } - } catch { - logger.error("apiDeleteChat error: \(responseError(error))") + let r = try await apiJoinGroup(groupId) + switch r { + case let .joined(groupInfo): + await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } + case .invitationRemoved: + AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") + await deleteGroup() + case .groupNotFound: + AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") + await deleteGroup() + } + } catch let error { + let err = responseError(error) + AlertManager.shared.showAlert(Alert(title: Text("Error joining group"), message: Text(err))) + logger.error("apiJoinGroup error: \(err)") + } + + func deleteGroup() async { + do { + // TODO this API should update chat item with the invitation as well + try await apiDeleteChat(type: .group, id: groupId) + await MainActor.run { ChatModel.shared.removeChat("#\(groupId)") } + } catch { + logger.error("apiDeleteChat error: \(responseError(error))") + } } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index def0d4202f..71bdd682c4 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -44,6 +44,17 @@ struct ChatListView: View { ToolbarItem(placement: .navigationBarLeading) { SettingsButton() } + ToolbarItem(placement: .principal) { + if (chatModel.incognito) { + HStack { + if (chatModel.chats.count > 8) { + Text("Your chats").font(.headline) + Spacer().frame(width: 16) + } + Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo) + } + } + } ToolbarItem(placement: .navigationBarTrailing) { switch chatModel.chatRunning { case .some(true): NewChatButton() diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index aca5e9e44a..8d591bfc83 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct ChatPreviewView: View { + @EnvironmentObject var chatModel: ChatModel @ObservedObject var chat: Chat @Environment(\.colorScheme) var colorScheme var darkGreen = Color(red: 0, green: 0.5, blue: 0) @@ -23,7 +24,6 @@ struct ChatPreviewView: View { .frame(width: 63, height: 63) chatPreviewImageOverlayIcon() .padding([.bottom, .trailing], 1) - } .padding(.leading, 4) @@ -89,7 +89,7 @@ struct ChatPreviewView: View { case .group(groupInfo: let groupInfo): switch (groupInfo.membership.memberStatus) { case .memInvited: - v.foregroundColor(.accentColor) + interactiveIncognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor) case .memAccepted: v.foregroundColor(.secondary) default: v @@ -98,6 +98,10 @@ struct ChatPreviewView: View { } } + private var interactiveIncognito: Bool { + chat.chatInfo.incognito || chatModel.incognito + } + @ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View { if let cItem = cItem { ZStack(alignment: .topTrailing) { @@ -127,7 +131,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { - case .memInvited: chatPreviewInfoText("you are invited to group") + case .memInvited: chatPreviewInfoText(groupInfo.membership.memberIncognito ? "you are invited to group incognito" : "you are invited to group") case .memAccepted: chatPreviewInfoText("connecting…") default: EmptyView() } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index 2edb1ad0ee..388666d2a5 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct ContactRequestView: View { + @EnvironmentObject var chatModel: ChatModel var contactRequest: UserContactRequest @ObservedObject var chat: Chat @@ -17,12 +18,13 @@ struct ContactRequestView: View { return HStack(spacing: 8) { ChatInfoImage(chat: chat) .frame(width: 63, height: 63) + .padding(.leading, 4) VStack(alignment: .leading, spacing: 4) { HStack(alignment: .top) { Text(contactRequest.chatViewName) .font(.title3) .fontWeight(.bold) - .foregroundColor(.blue) + .foregroundColor(chatModel.incognito ? .indigo : .accentColor) .padding(.leading, 8) .padding(.top, 4) .frame(maxHeight: .infinity, alignment: .topLeading) diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index a965d72dbb..3dd99ca9d3 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -10,6 +10,7 @@ import SwiftUI import CoreImage.CIFilterBuiltins struct AddContactView: View { + @EnvironmentObject var chatModel: ChatModel var connReqInvitation: String var body: some View { ScrollView { @@ -17,8 +18,23 @@ struct AddContactView: View { Text("One-time invitation link") .font(.title) .padding(.vertical) - Text("Your contact can scan it from the app") + Text("Your contact can scan it from the app.") + .padding(.bottom, 4) + if (chatModel.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to your contact").font(.footnote) + } .padding(.bottom) + } else { + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to your contact").font(.footnote) + } + .padding(.bottom) + } QRCode(uri: connReqInvitation) .padding(.bottom) Text("If you can't meet in person, **show QR code in the video call**, or share the link.") diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 7d278114ed..11e6948672 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -44,7 +44,21 @@ struct AddGroupView: View { .padding(.vertical, 4) Text("The group is fully decentralized – it is visible only to the members.") .padding(.bottom, 4) + if (m.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("You will use a random profile for this group").font(.footnote) + } .padding(.bottom) + } else { + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to group members").font(.footnote) + } + .padding(.bottom) + } ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift index 7bd64a90b5..05e36f63e8 100644 --- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift @@ -9,6 +9,7 @@ import SwiftUI struct PasteToConnectView: View { + @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @State private var connectionLink: String = "" @@ -18,8 +19,22 @@ struct PasteToConnectView: View { .font(.title) .padding(.vertical) Text("Paste the link you received into the box below to connect with your contact.") - Text("Your profile will be sent to the contact that you received this link from") + .padding(.bottom, 4) + if (chatModel.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to the contact that you received this link from").font(.footnote) + } .padding(.bottom) + } else { + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your profile will be sent to the contact that you received this link from").font(.footnote) + } + .padding(.bottom) + } TextEditor(text: $connectionLink) .onSubmit(connect) .textInputAutocapitalization(.never) @@ -55,7 +70,7 @@ struct PasteToConnectView: View { .frame(height: 48) .padding(.bottom) - Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button") + Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") } .padding() .frame(maxHeight: .infinity, alignment: .top) diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift index 49bce632b2..a70c74c095 100644 --- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift @@ -10,6 +10,7 @@ import SwiftUI import CodeScanner struct ScanToConnectView: View { + @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction var body: some View { @@ -17,8 +18,21 @@ struct ScanToConnectView: View { Text("Scan QR code") .font(.title) .padding(.vertical) - Text("Your chat profile will be sent to your contact") + if (chatModel.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to your contact").font(.footnote) + } .padding(.bottom) + } else { + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to your contact").font(.footnote) + } + .padding(.bottom) + } ZStack { CodeScannerView(codeTypes: [.qr], completion: processQRCode) .aspectRatio(1, contentMode: .fit) diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index 2406eb17f4..2f7be0eebf 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -39,12 +39,12 @@ struct CallSettings: View { } } } +} - private func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View { - ZStack(alignment: .topLeading) { - Text(n) - Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20) - } +func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View { + ZStack(alignment: .topLeading) { + Text(n) + Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20) } } diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift new file mode 100644 index 0000000000..32c548ed47 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -0,0 +1,57 @@ +// +// IncognitoHelp.swift +// SimpleX (iOS) +// +// Created by JRoberts on 22.08.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct IncognitoHelp: View { + var body: some View { + VStack(alignment: .leading) { + Text("Incognito mode") + .font(.largeTitle) + .padding(.vertical) + ScrollView { + VStack(alignment: .leading, spacing: 24) { + Text("Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.") + Text("It allows having many anonymous connections without any shared data between them in a single chat profile.") + Text("To find the profile used for an incognito connection, tap the contact or group name on top of the chat.") + } + .padding(.bottom) + + VStack(alignment: .leading, spacing: 8) { + Text("Incognito groups") + .font(.title2) + .padding(.top) + Text("When you join a group incognito, your new member profile is created and shared with all members.") + Group { + Text("Your are incognito in a group when:") + .padding(.top) + textListItem("•", "the group is created in Incognito mode,") + textListItem("•", "you or the member who invited you joined in Incognito mode,") + textListItem("•", "you have an incognito connection with the member who invited you.") + } + Group { + Text("Risks and limitations:") + .padding(.top) + textListItem("•", "It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.") + textListItem("•", "There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.") + + } + } + .padding(.bottom) + } + } + .frame(maxWidth: .infinity) + .padding() + } +} + +struct IncognitoHelp_Previews: PreviewProvider { + static var previews: some View { + IncognitoHelp() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index bd87e77ec2..4177e4496b 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -58,6 +58,7 @@ struct SettingsView: View { @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @State private var settingsSheet: SettingsSheet? var body: some View { let user: User = chatModel.currentUser! @@ -74,6 +75,8 @@ struct SettingsView: View { } .disabled(chatModel.chatRunning != true) + incognitoRow() + NavigationLink { UserAddress() .navigationTitle("Your chat address") @@ -202,9 +205,49 @@ struct SettingsView: View { } .navigationTitle("Your settings") } + .sheet(item: $settingsSheet) { sheet in + switch sheet { + case .incognitoInfo: IncognitoHelp() + } + } } - enum NotificationAlert { + @ViewBuilder private func incognitoRow() -> some View { + ZStack(alignment: .leading) { + Image(systemName: chatModel.incognito ? "theatermasks.fill" : "theatermasks") + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .foregroundColor(chatModel.incognito ? Color.indigo : .secondary) + Toggle(isOn: $chatModel.incognito) { + HStack { + Text("Incognito") + Spacer().frame(width: 4) + Image(systemName: "info.circle") + .foregroundColor(.accentColor) + .font(.system(size: 14)) + } + .onTapGesture { + settingsSheet = .incognitoInfo + } + } + .onChange(of: chatModel.incognito) { incognito in + incognitoGroupDefault.set(incognito) + do { + try apiSetIncognito(incognito: incognito) + } catch { + logger.error("apiSetIncognito: cannot set incognito \(responseError(error))") + } + } + .padding(.leading, indent) + } + } + + private enum SettingsSheet: Identifiable { + case incognitoInfo + + var id: SettingsSheet { get { self } } + } + + private enum NotificationAlert { case enable case error(LocalizedStringKey, String) } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 05d62cb5ec..a008b3d912 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -77,7 +77,7 @@ struct UserProfile: View { profileNameView("Display name:", user.profile.displayName) profileNameView("Full name:", user.profile.fullName) Button("Edit") { - profile = user.profile + profile = fromLocalProfile(user.profile) editProfile = true } } @@ -131,7 +131,7 @@ struct UserProfile: View { } func startEditingImage(_ user: User) { - profile = user.profile + profile = fromLocalProfile(user.profile) editProfile = true showChooseSource = true } @@ -141,7 +141,9 @@ struct UserProfile: View { do { if let newProfile = try await apiUpdateProfile(profile: profile) { DispatchQueue.main.async { - chatModel.currentUser?.profile = newProfile + if let profileId = chatModel.currentUser?.profile.profileId { + chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile) + } profile = newProfile } } diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 2756d8d15b..32ee0be336 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -5,6 +5,13 @@ + + + + + + No comment provided by engineer. + @@ -165,6 +172,16 @@ A new contact notification title + + A random profile will be sent to the contact that you received this link from + A random profile will be sent to the contact that you received this link from + No comment provided by engineer. + + + A random profile will be sent to your contact + A random profile will be sent to your contact + No comment provided by engineer. + About SimpleX About SimpleX @@ -175,6 +192,11 @@ About SimpleX Chat No comment provided by engineer. + + Accent color + Accent color + No comment provided by engineer. + Accept Accept @@ -191,6 +213,11 @@ Accept contact request from %@? notification body + + Accept incognito + Accept incognito + No comment provided by engineer. + Add contact to start a new chat Add contact to start a new chat @@ -261,6 +288,11 @@ Call already ended! No comment provided by engineer. + + Can't invite contact! + Can't invite contact! + No comment provided by engineer. + Cancel Cancel @@ -309,7 +341,7 @@ Chats Chats - back button to return to chats list + No comment provided by engineer. Choose file @@ -336,6 +368,11 @@ Clear conversation? No comment provided by engineer. + + Colors + Colors + No comment provided by engineer. + Configure SMP servers Configure SMP servers @@ -449,7 +486,7 @@ Copy Copy - No comment provided by engineer. + chat item action Create @@ -514,7 +551,7 @@ Delete Delete - No comment provided by engineer. + chat item action Delete Contact @@ -659,7 +696,7 @@ Edit Edit - No comment provided by engineer. + chat item action Edit group profile @@ -701,6 +738,11 @@ Error accessing database file No comment provided by engineer. + + Error adding member(s) + Error adding member(s) + No comment provided by engineer. + Error creating group Error creating group @@ -961,6 +1003,26 @@ In person or via a video call – the most secure way to connect. No comment provided by engineer. + + Incognito + Incognito + No comment provided by engineer. + + + Incognito groups + Incognito groups + No comment provided by engineer. + + + Incognito mode + Incognito mode + No comment provided by engineer. + + + Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. + Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. + No comment provided by engineer. + Incoming audio call Incoming audio call @@ -996,6 +1058,11 @@ Invitation expired! No comment provided by engineer. + + Invite anyway + Invite anyway + No comment provided by engineer. + Invite members Invite members @@ -1006,6 +1073,11 @@ Invite to group No comment provided by engineer. + + It allows having many anonymous connections without any shared data between them in a single chat profile. + It allows having many anonymous connections without any shared data between them in a single chat profile. + No comment provided by engineer. + It can happen when: 1. The messages expire on the server if they were not received for 30 days, @@ -1021,6 +1093,11 @@ Please connect to the developers via Settings to receive the updates about the s We will be adding server redundancy to prevent lost messages. No comment provided by engineer. + + It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. + It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). It seems like you are already connected via this link. If it is not the case, there was an error (%@). @@ -1036,16 +1113,31 @@ We will be adding server redundancy to prevent lost messages. Join No comment provided by engineer. + + Join anyway + Join anyway + No comment provided by engineer. + Join group Join group No comment provided by engineer. + + Join incognito + Join incognito + No comment provided by engineer. + Joining group Joining group No comment provided by engineer. + + Known main profile + Known main profile + No comment provided by engineer. + Large file! Large file! @@ -1091,6 +1183,11 @@ We will be adding server redundancy to prevent lost messages. Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* No comment provided by engineer. + + Mark read + Mark read + No comment provided by engineer. + Markdown in messages Markdown in messages @@ -1141,6 +1238,11 @@ We will be adding server redundancy to prevent lost messages. Most likely this contact has deleted the connection with you. No comment provided by engineer. + + Mute + Mute + No comment provided by engineer. + Network & servers Network & servers @@ -1181,6 +1283,11 @@ We will be adding server redundancy to prevent lost messages. New message notification + + No + No + No comment provided by engineer. + No contacts selected No contacts selected @@ -1236,6 +1343,21 @@ We will be adding server redundancy to prevent lost messages. One-time invitation link No comment provided by engineer. + + Onion hosts will be required for connection. Requires enabling VPN. + Onion hosts will be required for connection. Requires enabling VPN. + No comment provided by engineer. + + + Onion hosts will be used when available. Requires enabling VPN. + Onion hosts will be used when available. Requires enabling VPN. + No comment provided by engineer. + + + Onion hosts will not be used. + Onion hosts will not be used. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. @@ -1376,6 +1498,16 @@ We will be adding server redundancy to prevent lost messages. Reject contact request No comment provided by engineer. + + Relay server is only used if necessary. Another party can observe your IP address. + Relay server is only used if necessary. Another party can observe your IP address. + No comment provided by engineer. + + + Relay server protects your IP address, but it can observe the duration of the call. + Relay server protects your IP address, but it can observe the duration of the call. + No comment provided by engineer. + Remove Remove @@ -1394,6 +1526,16 @@ We will be adding server redundancy to prevent lost messages. Reply Reply + chat item action + + + Required + Required + No comment provided by engineer. + + + Reset colors + Reset colors No comment provided by engineer. @@ -1416,6 +1558,11 @@ We will be adding server redundancy to prevent lost messages. Revert No comment provided by engineer. + + Risks and limitations: + Risks and limitations: + No comment provided by engineer. + Run chat Run chat @@ -1434,7 +1581,7 @@ We will be adding server redundancy to prevent lost messages. Save Save - No comment provided by engineer. + chat item action Save (and notify contacts) @@ -1466,6 +1613,11 @@ We will be adding server redundancy to prevent lost messages. Scan contact's QR code No comment provided by engineer. + + Search + Search + No comment provided by engineer. + Send link previews Send link previews @@ -1509,7 +1661,7 @@ We will be adding server redundancy to prevent lost messages. Share Share - No comment provided by engineer. + chat item action Share invitation link @@ -1551,6 +1703,11 @@ We will be adding server redundancy to prevent lost messages. Skipped messages No comment provided by engineer. + + Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + No comment provided by engineer. + Somebody Somebody @@ -1621,6 +1778,11 @@ We will be adding server redundancy to prevent lost messages. Tap to join No comment provided by engineer. + + Tap to join incognito + Tap to join incognito + No comment provided by engineer. + Thank you for installing SimpleX Chat! Thank you for installing SimpleX Chat! @@ -1641,6 +1803,11 @@ We will be adding server redundancy to prevent lost messages. The connection you accepted will be cancelled! No comment provided by engineer. + + The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + No comment provided by engineer. + The contact you shared this link with will NOT be able to connect! The contact you shared this link with will NOT be able to connect! @@ -1681,6 +1848,11 @@ We will be adding server redundancy to prevent lost messages. The sender will NOT be notified No comment provided by engineer. + + There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. + There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. + No comment provided by engineer. + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. @@ -1701,6 +1873,11 @@ We will be adding server redundancy to prevent lost messages. To ask any questions and to receive updates: No comment provided by engineer. + + To find the profile used for an incognito connection, tap the contact or group name on top of the chat. + To find the profile used for an incognito connection, tap the contact or group name on top of the chat. + No comment provided by engineer. + To make a new connection To make a new connection @@ -1780,6 +1957,16 @@ To connect, please ask your contact to create another connection link and check Unlock authentication reason + + Unmute + Unmute + No comment provided by engineer. + + + Update .onion hosts setting? + Update .onion hosts setting? + No comment provided by engineer. + Update network settings? Update network settings? @@ -1790,6 +1977,16 @@ To connect, please ask your contact to create another connection link and check Updating settings will re-connect the client to all servers. No comment provided by engineer. + + Updating this setting will re-connect the client to all servers. + Updating this setting will re-connect the client to all servers. + No comment provided by engineer. + + + Use .onion hosts + Use .onion hosts + No comment provided by engineer. + Use SimpleX Chat servers? Use SimpleX Chat servers? @@ -1800,11 +1997,21 @@ To connect, please ask your contact to create another connection link and check Use chat No comment provided by engineer. + + Using .onion hosts requires compatible VPN provider. + Using .onion hosts requires compatible VPN provider. + No comment provided by engineer. + Using SimpleX Chat servers. Using SimpleX Chat servers. No comment provided by engineer. + + Video call + Video call + No comment provided by engineer. + Waiting for file Waiting for file @@ -1820,6 +2027,16 @@ To connect, please ask your contact to create another connection link and check Welcome %@! No comment provided by engineer. + + When available + When available + No comment provided by engineer. + + + When you join a group incognito, your new member profile is created and shared with all members. + When you join a group incognito, your new member profile is created and shared with all members. + No comment provided by engineer. + You You @@ -1845,9 +2062,9 @@ To connect, please ask your contact to create another connection link and check You are invited to group No comment provided by engineer. - - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button + + You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. + You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. No comment provided by engineer. @@ -1915,6 +2132,11 @@ To connect, please ask your contact to create another connection link and check You sent group invitation No comment provided by engineer. + + You sent group invitation incognito + You sent group invitation incognito + No comment provided by engineer. + You will be connected when your connection request is accepted, please wait or check later! You will be connected when your connection request is accepted, please wait or check later! @@ -1935,6 +2157,16 @@ To connect, please ask your contact to create another connection link and check You will stop receiving messages from this group. Chat history will be preserved. No comment provided by engineer. + + You will use a random profile for this group + You will use a random profile for this group + No comment provided by engineer. + + + You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile + You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile + No comment provided by engineer. + Your SMP servers Your SMP servers @@ -1945,6 +2177,11 @@ To connect, please ask your contact to create another connection link and check Your SimpleX contact address No comment provided by engineer. + + Your are incognito in a group when: + Your are incognito in a group when: + No comment provided by engineer. + Your calls Your calls @@ -1965,6 +2202,11 @@ To connect, please ask your contact to create another connection link and check Your chat profile No comment provided by engineer. + + Your chat profile will be sent to group members + Your chat profile will be sent to group members + No comment provided by engineer. + Your chat profile will be sent to your contact Your chat profile will be sent to your contact @@ -1975,9 +2217,9 @@ To connect, please ask your contact to create another connection link and check Your chats No comment provided by engineer. - - Your contact can scan it from the app - Your contact can scan it from the app + + Your contact can scan it from the app. + Your contact can scan it from the app. No comment provided by engineer. @@ -1997,6 +2239,11 @@ You can cancel this connection and remove the contact (and try later with a new Your current chat database will be DELETED and REPLACED with the imported one. No comment provided by engineer. + + Your main profile may be shared + Your main profile may be shared + No comment provided by engineer. + Your privacy Your privacy @@ -2019,6 +2266,11 @@ SimpleX servers cannot see your profile. Your profile, contacts and delivered messages are stored on your device. No comment provided by engineer. + + Your random profile + Your random profile + No comment provided by engineer. + Your settings Your settings @@ -2219,6 +2471,21 @@ SimpleX servers cannot see your profile. group profile updated snd group event chat item + + incognito invitation to group %@ + incognito invitation to group %@ + group name + + + incognito via contact address link + incognito via contact address link + chat list item description + + + incognito via one-time link + incognito via one-time link + chat list item description + indirect (%d) indirect (%d) @@ -2249,6 +2516,11 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. + + known to you as %@ connected incognito + known to you as %@ connected incognito + rcv group event chat item + left left @@ -2349,6 +2621,11 @@ SimpleX servers cannot see your profile. strike No comment provided by engineer. + + the group is created in Incognito mode, + the group is created in Incognito mode, + No comment provided by engineer. + this contact this contact @@ -2409,11 +2686,26 @@ SimpleX servers cannot see your profile. you are invited to group No comment provided by engineer. + + you are invited to group incognito + you are invited to group incognito + No comment provided by engineer. + + + you have an incognito connection with the member who invited you. + you have an incognito connection with the member who invited you. + No comment provided by engineer. + you left you left snd group event chat item + + you or the member who invited you joined in Incognito mode, + you or the member who invited you joined in Incognito mode, + No comment provided by engineer. + you removed %@ you removed %@ @@ -2424,6 +2716,11 @@ SimpleX servers cannot see your profile. you shared one-time link chat list item description + + you shared one-time link incognito + you shared one-time link incognito + chat list item description + you: you: diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 474183cc26..50ab0d6b07 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -5,6 +5,13 @@ + + + + + + No comment provided by engineer. + @@ -165,6 +172,16 @@ Новый контакт notification title + + A random profile will be sent to the contact that you received this link from + Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль + No comment provided by engineer. + + + A random profile will be sent to your contact + Вашему контакту будет отправлен случайный профиль + No comment provided by engineer. + About SimpleX О SimpleX @@ -175,6 +192,11 @@ Информация о SimpleX Chat No comment provided by engineer. + + Accent color + Цвет акцента + No comment provided by engineer. + Accept Принять @@ -191,6 +213,11 @@ Принять запрос на соединение от %@? notification body + + Accept incognito + Принять инкогнито + No comment provided by engineer. + Add contact to start a new chat Добавьте контакт, чтобы начать разговор @@ -261,6 +288,11 @@ Звонок уже завершен! No comment provided by engineer. + + Can't invite contact! + Нельзя пригласить контакт! + No comment provided by engineer. + Cancel Отменить @@ -309,7 +341,7 @@ Chats Чаты - back button to return to chats list + No comment provided by engineer. Choose file @@ -336,6 +368,11 @@ Очистить разговор? No comment provided by engineer. + + Colors + Цвета + No comment provided by engineer. + Configure SMP servers Настройка SMP серверов @@ -449,7 +486,7 @@ Copy Скопировать - No comment provided by engineer. + chat item action Create @@ -514,7 +551,7 @@ Delete Удалить - No comment provided by engineer. + chat item action Delete Contact @@ -659,7 +696,7 @@ Edit Редактировать - No comment provided by engineer. + chat item action Edit group profile @@ -701,6 +738,11 @@ Ошибка при доступе к данным чата No comment provided by engineer. + + Error adding member(s) + Ошибка при добавлении членов группы + No comment provided by engineer. + Error creating group Ошибка при создании группы @@ -961,6 +1003,26 @@ При встрече или в видеозвонке – самый безопасный способ установить соединение No comment provided by engineer. + + Incognito + Инкогнито + No comment provided by engineer. + + + Incognito groups + Инкогнито группы + No comment provided by engineer. + + + Incognito mode + Режим Инкогнито + No comment provided by engineer. + + + Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. + Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль. + No comment provided by engineer. + Incoming audio call Входящий аудиозвонок @@ -996,6 +1058,11 @@ Приглашение истекло! No comment provided by engineer. + + Invite anyway + Пригласить + No comment provided by engineer. + Invite members Пригласить членов группы @@ -1006,6 +1073,11 @@ Пригласить в группу No comment provided by engineer. + + It allows having many anonymous connections without any shared data between them in a single chat profile. + Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя. + No comment provided by engineer. + It can happen when: 1. The messages expire on the server if they were not received for 30 days, @@ -1021,6 +1093,11 @@ We will be adding server redundancy to prevent lost messages. Мы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения. No comment provided by engineer. + + It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. + Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@). @@ -1036,16 +1113,31 @@ We will be adding server redundancy to prevent lost messages. Вступить No comment provided by engineer. + + Join anyway + Вступить + No comment provided by engineer. + Join group Вступить в группу No comment provided by engineer. + + Join incognito + Вступить инкогнито + No comment provided by engineer. + Joining group Вступление в группу No comment provided by engineer. + + Known main profile + Известный основной профиль + No comment provided by engineer. + Large file! Большой файл! @@ -1091,6 +1183,11 @@ We will be adding server redundancy to prevent lost messages. Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?* No comment provided by engineer. + + Mark read + Прочитано + No comment provided by engineer. + Markdown in messages Форматирование сообщений @@ -1141,6 +1238,11 @@ We will be adding server redundancy to prevent lost messages. Скорее всего, этот контакт удалил соединение с вами. No comment provided by engineer. + + Mute + Без звука + No comment provided by engineer. + Network & servers Сеть & серверы @@ -1181,6 +1283,11 @@ We will be adding server redundancy to prevent lost messages. Новое сообщение notification + + No + Нет + No comment provided by engineer. + No contacts selected Контакты не выбраны @@ -1236,6 +1343,21 @@ We will be adding server redundancy to prevent lost messages. Одноразовая ссылка No comment provided by engineer. + + Onion hosts will be required for connection. Requires enabling VPN. + Подключаться только к onion хостам. Требуется включенный VPN. + No comment provided by engineer. + + + Onion hosts will be used when available. Requires enabling VPN. + Onion хосты используются, если возможно. Требуется включенный VPN. + No comment provided by engineer. + + + Onion hosts will not be used. + Onion хосты не используются. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием** @@ -1376,6 +1498,16 @@ We will be adding server redundancy to prevent lost messages. Отклонить запрос No comment provided by engineer. + + Relay server is only used if necessary. Another party can observe your IP address. + Relay сервер используется только при необходимости. Другая сторона может видеть ваш IP адрес. + No comment provided by engineer. + + + Relay server protects your IP address, but it can observe the duration of the call. + Relay сервер защищает ваш IP адрес, но может отслеживать продолжительность звонка. + No comment provided by engineer. + Remove Удалить @@ -1394,6 +1526,16 @@ We will be adding server redundancy to prevent lost messages. Reply Ответить + chat item action + + + Required + Обязательно + No comment provided by engineer. + + + Reset colors + Сбросить цвета No comment provided by engineer. @@ -1416,6 +1558,11 @@ We will be adding server redundancy to prevent lost messages. Отменить изменения No comment provided by engineer. + + Risks and limitations: + Риски и ограничения: + No comment provided by engineer. + Run chat Запустить chat @@ -1434,7 +1581,7 @@ We will be adding server redundancy to prevent lost messages. Save Сохранить - No comment provided by engineer. + chat item action Save (and notify contacts) @@ -1466,6 +1613,11 @@ We will be adding server redundancy to prevent lost messages. Сосканировать QR код контакта No comment provided by engineer. + + Search + Поиск + No comment provided by engineer. + Send link previews Отправлять картинки ссылок @@ -1509,7 +1661,7 @@ We will be adding server redundancy to prevent lost messages. Share Поделиться - No comment provided by engineer. + chat item action Share invitation link @@ -1551,6 +1703,11 @@ We will be adding server redundancy to prevent lost messages. Пропущенные сообщения No comment provided by engineer. + + Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля. + No comment provided by engineer. + Somebody Контакт @@ -1621,6 +1778,11 @@ We will be adding server redundancy to prevent lost messages. Нажмите, чтобы вступить No comment provided by engineer. + + Tap to join incognito + Нажмите, чтобы вступить инкогнито + No comment provided by engineer. + Thank you for installing SimpleX Chat! Спасибо, что установили SimpleX Chat! @@ -1641,6 +1803,11 @@ We will be adding server redundancy to prevent lost messages. Подтвержденное соединение будет отменено! No comment provided by engineer. + + The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. + У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля. + No comment provided by engineer. + The contact you shared this link with will NOT be able to connect! Контакт, которому вы отправили эту ссылку, не сможет соединиться! @@ -1681,6 +1848,11 @@ We will be adding server redundancy to prevent lost messages. Отправитель не будет уведомлён No comment provided by engineer. + + There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. + Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение. + No comment provided by engineer. + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. @@ -1701,6 +1873,11 @@ We will be adding server redundancy to prevent lost messages. Чтобы задать вопросы и получать уведомления о новых версиях, No comment provided by engineer. + + To find the profile used for an incognito connection, tap the contact or group name on top of the chat. + Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата. + No comment provided by engineer. + To make a new connection Чтобы соединиться @@ -1780,6 +1957,16 @@ To connect, please ask your contact to create another connection link and check Разблокировать authentication reason + + Unmute + Уведомлять + No comment provided by engineer. + + + Update .onion hosts setting? + Обновить настройки .onion хостов? + No comment provided by engineer. + Update network settings? Обновить настройки сети? @@ -1787,7 +1974,17 @@ To connect, please ask your contact to create another connection link and check Updating settings will re-connect the client to all servers. - Обновление настроек приведет к переподключению клиента ко всем серверам. + Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. + No comment provided by engineer. + + + Updating this setting will re-connect the client to all servers. + Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами. + No comment provided by engineer. + + + Use .onion hosts + Использовать .onion хосты No comment provided by engineer. @@ -1800,11 +1997,21 @@ To connect, please ask your contact to create another connection link and check Использовать чат No comment provided by engineer. + + Using .onion hosts requires compatible VPN provider. + Для использования .onion хостов требуется совместимый VPN провайдер. + No comment provided by engineer. + Using SimpleX Chat servers. Используются серверы, предоставленные SimpleX Chat. No comment provided by engineer. + + Video call + Видеозвонок + No comment provided by engineer. + Waiting for file Ожидается прием файла @@ -1820,6 +2027,16 @@ To connect, please ask your contact to create another connection link and check Здравствуйте %@! No comment provided by engineer. + + When available + Когда возможно + No comment provided by engineer. + + + When you join a group incognito, your new member profile is created and shared with all members. + Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы. + No comment provided by engineer. + You Вы @@ -1845,8 +2062,8 @@ To connect, please ask your contact to create another connection link and check Вы приглашены в группу No comment provided by engineer. - - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button + + You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**. No comment provided by engineer. @@ -1915,6 +2132,11 @@ To connect, please ask your contact to create another connection link and check Вы отправили приглашение в группу No comment provided by engineer. + + You sent group invitation incognito + Вы отправили приглашение в группу инкогнито + No comment provided by engineer. + You will be connected when your connection request is accepted, please wait or check later! Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! @@ -1935,6 +2157,16 @@ To connect, please ask your contact to create another connection link and check Вы перестанете получать сообщения от этой группы. История чата будет сохранена. No comment provided by engineer. + + You will use a random profile for this group + Вы будете использовать случайный профиль для этой группы + No comment provided by engineer. + + + You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile + Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль + No comment provided by engineer. + Your SMP servers Ваши SMP серверы @@ -1945,6 +2177,11 @@ To connect, please ask your contact to create another connection link and check Ваш SimpleX адрес No comment provided by engineer. + + Your are incognito in a group when: + Вы инкогнито в группе, когда: + No comment provided by engineer. + Your calls Ваши звонки @@ -1965,6 +2202,11 @@ To connect, please ask your contact to create another connection link and check Ваш профиль No comment provided by engineer. + + Your chat profile will be sent to group members + Ваш профиль чата будет отправлен участникам группы + No comment provided by engineer. + Your chat profile will be sent to your contact Ваш профиль будет отправлен вашему контакту @@ -1975,9 +2217,9 @@ To connect, please ask your contact to create another connection link and check Ваши чаты No comment provided by engineer. - - Your contact can scan it from the app - Ваш контакт может сосканировать QR в приложении + + Your contact can scan it from the app. + Ваш контакт может сосканировать QR код в приложении No comment provided by engineer. @@ -1997,6 +2239,11 @@ You can cancel this connection and remove the contact (and try later with a new Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. No comment provided by engineer. + + Your main profile may be shared + Вашим основным профилем могут поделиться + No comment provided by engineer. + Your privacy Конфиденциальность @@ -2019,6 +2266,11 @@ SimpleX серверы не могут получить доступ к ваше Ваш профиль, контакты и доставленные сообщения хранятся на вашем устройстве. No comment provided by engineer. + + Your random profile + Ваш случайный профиль + No comment provided by engineer. + Your settings Настройки @@ -2219,6 +2471,21 @@ SimpleX серверы не могут получить доступ к ваше профиль группы обновлен snd group event chat item + + incognito invitation to group %@ + инкогнито приглашение в группу %@ + group name + + + incognito via contact address link + инкогнито через ссылку-контакт + chat list item description + + + incognito via one-time link + инкогнито через одноразовую ссылку + chat list item description + indirect (%d) непрямое (%d) @@ -2249,6 +2516,11 @@ SimpleX серверы не могут получить доступ к ваше курсив No comment provided by engineer. + + known to you as %@ connected incognito + известный(ая) вам как %@ соединен(а) инкогнито + rcv group event chat item + left покинул(а) группу @@ -2349,6 +2621,11 @@ SimpleX серверы не могут получить доступ к ваше зачеркнуть No comment provided by engineer. + + the group is created in Incognito mode, + группа создана в режиме Инкогнито, + No comment provided by engineer. + this contact этот контакт @@ -2409,11 +2686,26 @@ SimpleX серверы не могут получить доступ к ваше вы приглашены в группу No comment provided by engineer. + + you are invited to group incognito + вы приглашены в группу инкогнито + No comment provided by engineer. + + + you have an incognito connection with the member who invited you. + вы соединены инкогнито с членом группы, который вас пригласил. + No comment provided by engineer. + you left вы покинули группу snd group event chat item + + you or the member who invited you joined in Incognito mode, + вы или пригласивший вас член группы вступили в группу в режиме Инкогнито, + No comment provided by engineer. + you removed %@ вы удалили %@ @@ -2424,6 +2716,11 @@ SimpleX серверы не могут получить доступ к ваше вы создали ссылку chat list item description + + you shared one-time link incognito + вы создали ссылку инкогнито + chat list item description + you: вы: diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index be039a65e5..969645e45f 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -160,6 +160,7 @@ func startChat() -> User? { let justStarted = try apiStartChat() if justStarted { try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetIncognito(incognito: incognitoGroupDefault.get()) chatLastStartGroupDefault.set(Date.now) Task { await receiveMessages() } } @@ -248,6 +249,12 @@ func apiSetFilesFolder(filesFolder: String) throws { throw r } +func apiSetIncognito(incognito: Bool) throws { + let r = sendSimpleXCmd(.setIncognito(incognito: incognito)) + if case .cmdOk = r { return } + throw r +} + func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 5f435b04a4..eccc152754 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -13,11 +13,6 @@ 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; - 5C00165628B02AF40094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165128B02AF30094D739 /* libffi.a */; }; - 5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */; }; - 5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */; }; - 5C00165928B02AF40094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165428B02AF30094D739 /* libgmp.a */; }; - 5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165528B02AF30094D739 /* libgmpxx.a */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -131,6 +126,12 @@ 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; + 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; + 64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */; }; + 64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */; }; + 64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3E28B3A99900CD1FB1 /* libgmp.a */; }; + 64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */; }; + 64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC4028B3A99A00CD1FB1 /* libffi.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -196,11 +197,6 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; - 5C00165128B02AF30094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a"; sourceTree = ""; }; - 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a"; sourceTree = ""; }; - 5C00165428B02AF30094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C00165528B02AF30094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -317,6 +313,12 @@ 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; + 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; + 64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a"; sourceTree = ""; }; + 64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 64F1CC3E28B3A99900CD1FB1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a"; sourceTree = ""; }; + 64F1CC4028B3A99A00CD1FB1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -349,13 +351,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */, + 64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */, + 64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */, + 64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */, + 64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */, - 5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */, - 5C00165628B02AF40094D739 /* libffi.a in Frameworks */, - 5C00165928B02AF40094D739 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -410,11 +412,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C00165128B02AF30094D739 /* libffi.a */, - 5C00165428B02AF30094D739 /* libgmp.a */, - 5C00165528B02AF30094D739 /* libgmpxx.a */, - 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */, - 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */, + 64F1CC4028B3A99A00CD1FB1 /* libffi.a */, + 64F1CC3E28B3A99900CD1FB1 /* libgmp.a */, + 64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */, + 64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */, + 64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */, ); path = Libraries; sourceTree = ""; @@ -553,6 +555,7 @@ 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 640F50E227CF991C001E05C2 /* SMPServers.swift */, 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */, + 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, ); path = UserSettings; sourceTree = ""; @@ -869,6 +872,7 @@ 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */, + 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 83ca4bb955..5fb8cb19ed 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -20,6 +20,7 @@ public enum ChatCommand { case apiActivateChat case apiSuspendChat(timeoutMicroseconds: Int) case setFilesFolder(filesFolder: String) + case setIncognito(incognito: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -82,6 +83,7 @@ public enum ChatCommand { case .apiActivateChat: return "/_app activate" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" + case let .setIncognito(incognito): return "/incognito \(incognito ? "on" : "off")" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -148,6 +150,7 @@ public enum ChatCommand { case .apiActivateChat: return "apiActivateChat" case .apiSuspendChat: return "apiSuspendChat" case .setFilesFolder: return "setFilesFolder" + case .setIncognito: return "setIncognito" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -225,8 +228,8 @@ public enum ChatResponse: Decodable, Error { case apiChat(chat: ChatData) case userSMPServers(smpServers: [String]) case networkConfig(networkConfig: NetCfg) - case contactInfo(contact: Contact, connectionStats: ConnectionStats) - case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) + case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?, mainProfile: Profile?) case invitation(connReqInvitation: String) case sentConfirmation case sentInvitation @@ -407,8 +410,8 @@ public enum ChatResponse: Decodable, Error { case let .apiChat(chat): return String(describing: chat) case let .userSMPServers(smpServers): return String(describing: smpServers) case let .networkConfig(networkConfig): return String(describing: networkConfig) - case let .contactInfo(contact, connectionStats): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))" - case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\\nconnectionStats_: \(String(describing: connectionStats_))" + case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))" + case let .groupMemberInfo(groupInfo, member, connectionStats_, mainProfile): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))\nmainProfile: \(String(describing: mainProfile))" case let .invitation(connReqInvitation): return connReqInvitation case .sentConfirmation: return noDetails case .sentInvitation: return noDetails diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 15ff7ee52a..d428c00832 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -23,6 +23,7 @@ let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle" let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" +let GROUP_DEFAULT_INCOGNITO = "incognito" let APP_GROUP_NAME = "group.chat.simplex.app" @@ -37,7 +38,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive, GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle, GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl, - GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt + GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt, + GROUP_DEFAULT_INCOGNITO: false ]) } @@ -82,6 +84,8 @@ public let ntfPreviewModeGroupDefault = EnumDefault( withDefault: .message ) +public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO) + public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 267d27d03a..b5fea9e1bb 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -13,7 +13,7 @@ public struct User: Decodable, NamedChat { var userId: Int64 var userContactId: Int64 var localDisplayName: ContactName - public var profile: Profile + public var profile: LocalProfile var activeUser: Bool public var displayName: String { get { profile.displayName } } @@ -24,7 +24,7 @@ public struct User: Decodable, NamedChat { userId: 1, userContactId: 1, localDisplayName: "alice", - profile: Profile.sampleData, + profile: LocalProfile.sampleData, activeUser: true ) } @@ -54,6 +54,38 @@ public struct Profile: Codable, NamedChat { ) } +public struct LocalProfile: Codable, NamedChat { + public init(profileId: Int64, displayName: String, fullName: String, image: String? = nil) { + self.profileId = profileId + self.displayName = displayName + self.fullName = fullName + self.image = image + } + + public var profileId: Int64 + public var displayName: String + public var fullName: String + public var image: String? + + var profileViewName: String { + (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" + } + + static let sampleData = LocalProfile( + profileId: 1, + displayName: "alice", + fullName: "Alice" + ) +} + +public func toLocalProfile (_ profileId: Int64, _ profile: Profile ) -> LocalProfile { + LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image) +} + +public func fromLocalProfile (_ profile: LocalProfile) -> Profile { + Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image) +} + public enum ChatType: String { case direct = "@" case group = "#" @@ -180,6 +212,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var incognito: Bool { + get { + switch self { + case let .direct(contact): return contact.contactConnIncognito + case let .group(groupInfo): return groupInfo.membership.memberIncognito + case .contactRequest: return false + case let .contactConnection(contactConnection): return contactConnection.incognito + } + } + } + public var contact: Contact? { get { switch self { @@ -249,7 +292,7 @@ public struct ChatStats: Decodable { public struct Contact: Identifiable, Decodable, NamedChat { var contactId: Int64 var localDisplayName: ContactName - public var profile: Profile + public var profile: LocalProfile public var activeConn: Connection public var viaGroup: Int64? public var chatSettings: ChatSettings @@ -264,14 +307,18 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } - public func isIndirectContact() -> Bool { - return activeConn.connLevel > 0 || viaGroup != nil + public var isIndirectContact: Bool { + activeConn.connLevel > 0 || viaGroup != nil + } + + public var contactConnIncognito: Bool { + activeConn.customUserProfileId != nil } public static let sampleData = Contact( contactId: 1, localDisplayName: "alice", - profile: Profile.sampleData, + profile: LocalProfile.sampleData, activeConn: Connection.sampleData, chatSettings: ChatSettings.defaults, createdAt: .now, @@ -295,6 +342,7 @@ public struct Connection: Decodable { var connId: Int64 var connStatus: ConnStatus public var connLevel: Int + public var customUserProfileId: Int64? public var id: ChatId { get { ":\(connId)" } } @@ -352,6 +400,7 @@ public struct PendingContactConnection: Decodable, NamedChat { var pccAgentConnId: String var pccConnStatus: ConnStatus public var viaContactUri: Bool + public var customUserProfileId: Int64? var createdAt: Date public var updatedAt: Date @@ -378,14 +427,34 @@ public struct PendingContactConnection: Decodable, NamedChat { public var image: String? { get { nil } } public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } + public var incognito: Bool { + customUserProfileId != nil + } + public var description: String { get { if let initiated = pccConnStatus.initiated { - return initiated && !viaContactUri - ? NSLocalizedString("you shared one-time link", comment: "chat list item description") - : viaContactUri - ? NSLocalizedString("via contact address link", comment: "chat list item description") - : NSLocalizedString("via one-time link", comment: "chat list item description") + var desc: String + if initiated && !viaContactUri { + if incognito { + desc = NSLocalizedString("you shared one-time link incognito", comment: "chat list item description") + } else { + desc = NSLocalizedString("you shared one-time link", comment: "chat list item description") + } + } else if viaContactUri { + if incognito { + desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description") + } else { + desc = NSLocalizedString("via contact address link", comment: "chat list item description") + } + } else { + if incognito { + desc = NSLocalizedString("incognito via one-time link", comment: "chat list item description") + } else { + desc = NSLocalizedString("via one-time link", comment: "chat list item description") + } + } + return desc } else { return "" } @@ -443,6 +512,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { var localDisplayName: GroupName public var groupProfile: GroupProfile public var membership: GroupMember + public var hostConnCustomUserProfileId: Int64? public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date @@ -472,6 +542,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { localDisplayName: "team", groupProfile: GroupProfile.sampleData, membership: GroupMember.sampleData, + hostConnCustomUserProfileId: nil, chatSettings: ChatSettings.defaults, createdAt: .now, updatedAt: .now @@ -504,8 +575,9 @@ public struct GroupMember: Identifiable, Decodable { public var memberStatus: GroupMemberStatus public var invitedBy: InvitedBy public var localDisplayName: ContactName - public var memberProfile: Profile + public var memberProfile: LocalProfile public var memberContactId: Int64? + public var memberContactProfileId: Int64 public var activeConn: Connection? public var id: String { "#\(groupId) @\(groupMemberId)" } @@ -568,6 +640,10 @@ public struct GroupMember: Identifiable, Decodable { && userRole >= .admin && userRole >= memberRole && membership.memberCurrent } + public var memberIncognito: Bool { + memberProfile.profileId != memberContactProfileId + } + public static let sampleData = GroupMember( groupMemberId: 1, groupId: 1, @@ -577,8 +653,9 @@ public struct GroupMember: Identifiable, Decodable { memberStatus: .memComplete, invitedBy: .user, localDisplayName: "alice", - memberProfile: Profile.sampleData, + memberProfile: LocalProfile.sampleData, memberContactId: 1, + memberContactProfileId: 1, activeConn: Connection.sampleData ) } @@ -1280,9 +1357,12 @@ public struct CIGroupInvitation: Decodable { public var localDisplayName: GroupName public var groupProfile: GroupProfile public var status: CIGroupInvitationStatus + public var invitedIncognito: Bool? var text: String { - String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName) + (invitedIncognito ?? false) ? + String.localizedStringWithFormat(NSLocalizedString("incognito invitation to group %@", comment: "group name"), groupProfile.displayName) + : String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName) } public static func getSample(groupId: Int64 = 1, groupMemberId: Int64 = 1, localDisplayName: GroupName = "team", groupProfile: GroupProfile = GroupProfile.sampleData, status: CIGroupInvitationStatus = .pending) -> CIGroupInvitation { @@ -1299,7 +1379,7 @@ public enum CIGroupInvitationStatus: String, Decodable { public enum RcvGroupEvent: Decodable { case memberAdded(groupMemberId: Int64, profile: Profile) - case memberConnected + case memberConnected(contactMainProfile: Profile?) case memberLeft case memberDeleted(groupMemberId: Int64, profile: Profile) case userDeleted @@ -1310,7 +1390,12 @@ public enum RcvGroupEvent: Decodable { switch self { case let .memberAdded(_, profile): return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) - case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item") + case let .memberConnected(contactMainProfile): + if let contactMainProfile = contactMainProfile { + return String.localizedStringWithFormat(NSLocalizedString("known to you as %@ connected incognito", comment: "rcv group event chat item"), contactMainProfile.profileViewName) + } else { + return NSLocalizedString("member connected", comment: "rcv group event chat item") + } case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 6688e09b47..2e84149b57 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + /* No comment provided by engineer. */ " " = " "; @@ -106,6 +109,12 @@ /* notification title */ "A new contact" = "Новый контакт"; +/* No comment provided by engineer. */ +"A random profile will be sent to the contact that you received this link from" = "Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль"; + +/* No comment provided by engineer. */ +"A random profile will be sent to your contact" = "Вашему контакту будет отправлен случайный профиль"; + /* No comment provided by engineer. */ "About SimpleX" = "О SimpleX"; @@ -115,6 +124,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "наверху, затем выберите:"; +/* No comment provided by engineer. */ +"Accent color" = "Цвет акцента"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Принять"; @@ -125,6 +137,9 @@ /* notification body */ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; +/* No comment provided by engineer. */ +"Accept incognito" = "Принять инкогнито"; + /* call status */ "accepted call" = " принятый звонок"; @@ -194,6 +209,9 @@ /* call status */ "calling…" = "входящий звонок…"; +/* No comment provided by engineer. */ +"Can't invite contact!" = "Нельзя пригласить контакт!"; + /* No comment provided by engineer. */ "Cancel" = "Отменить"; @@ -221,7 +239,7 @@ /* No comment provided by engineer. */ "Chat with the developers" = "Соединиться с разработчиками"; -/* back button to return to chats list */ +/* No comment provided by engineer. */ "Chats" = "Чаты"; /* No comment provided by engineer. */ @@ -242,6 +260,9 @@ /* No comment provided by engineer. */ "colored" = "цвет"; +/* No comment provided by engineer. */ +"Colors" = "Цвета"; + /* No comment provided by engineer. */ "complete" = "соединение завершено"; @@ -350,7 +371,7 @@ /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; -/* No comment provided by engineer. */ +/* chat item action */ "Copy" = "Скопировать"; /* No comment provided by engineer. */ @@ -392,7 +413,7 @@ /* No comment provided by engineer. */ "Decentralized" = "Децентрализованный"; -/* No comment provided by engineer. */ +/* chat item action */ "Delete" = "Удалить"; /* No comment provided by engineer. */ @@ -494,7 +515,7 @@ /* No comment provided by engineer. */ "e2e encrypted" = "e2e зашифровано"; -/* No comment provided by engineer. */ +/* chat item action */ "Edit" = "Редактировать"; /* No comment provided by engineer. */ @@ -530,6 +551,9 @@ /* No comment provided by engineer. */ "Error accessing database file" = "Ошибка при доступе к данным чата"; +/* No comment provided by engineer. */ +"Error adding member(s)" = "Ошибка при добавлении членов группы"; + /* No comment provided by engineer. */ "Error creating group" = "Ошибка при создании группы"; @@ -692,6 +716,27 @@ /* No comment provided by engineer. */ "In person or via a video call – the most secure way to connect." = "При встрече или в видеозвонке – самый безопасный способ установить соединение"; +/* No comment provided by engineer. */ +"Incognito" = "Инкогнито"; + +/* No comment provided by engineer. */ +"Incognito groups" = "Инкогнито группы"; + +/* group name */ +"incognito invitation to group %@" = "инкогнито приглашение в группу %@"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Режим Инкогнито"; + +/* No comment provided by engineer. */ +"Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль."; + +/* chat list item description */ +"incognito via contact address link" = "инкогнито через ссылку-контакт"; + +/* chat list item description */ +"incognito via one-time link" = "инкогнито через одноразовую ссылку"; + /* notification */ "Incoming audio call" = "Входящий аудиозвонок"; @@ -719,6 +764,9 @@ /* group name */ "invitation to group %@" = "приглашение в группу %@"; +/* No comment provided by engineer. */ +"Invite anyway" = "Пригласить"; + /* No comment provided by engineer. */ "Invite members" = "Пригласить членов группы"; @@ -734,9 +782,15 @@ /* chat list item title */ "invited to connect" = "приглашение"; +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; + /* No comment provided by engineer. */ "It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages." = "Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения."; +/* No comment provided by engineer. */ +"It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile." = "Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -749,12 +803,24 @@ /* No comment provided by engineer. */ "Join" = "Вступить"; +/* No comment provided by engineer. */ +"Join anyway" = "Вступить"; + /* No comment provided by engineer. */ "Join group" = "Вступить в группу"; +/* No comment provided by engineer. */ +"Join incognito" = "Вступить инкогнито"; + /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; +/* No comment provided by engineer. */ +"Known main profile" = "Известный основной профиль"; + +/* rcv group event chat item */ +"known to you as %@ connected incognito" = "известный(ая) вам как %@ соединен(а) инкогнито"; + /* No comment provided by engineer. */ "Large file!" = "Большой файл!"; @@ -785,6 +851,9 @@ /* No comment provided by engineer. */ "Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*"; +/* No comment provided by engineer. */ +"Mark read" = "Прочитано"; + /* No comment provided by engineer. */ "Markdown in messages" = "Форматирование сообщений"; @@ -827,6 +896,9 @@ /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с вами."; +/* No comment provided by engineer. */ +"Mute" = "Без звука"; + /* No comment provided by engineer. */ "Network & servers" = "Сеть & серверы"; @@ -854,6 +926,9 @@ /* notification */ "New message" = "Новое сообщение"; +/* No comment provided by engineer. */ +"No" = "Нет"; + /* No comment provided by engineer. */ "No contacts selected" = "Контакты не выбраны"; @@ -890,6 +965,15 @@ /* No comment provided by engineer. */ "One-time invitation link" = "Одноразовая ссылка"; +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "Подключаться только к onion хостам. Требуется включенный VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Onion хосты используются, если возможно. Требуется включенный VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion хосты не используются."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**"; @@ -992,6 +1076,12 @@ /* call status */ "rejected call" = "отклонённый звонок"; +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Relay сервер используется только при необходимости. Другая сторона может видеть ваш IP адрес."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Relay сервер защищает ваш IP адрес, но может отслеживать продолжительность звонка."; + /* No comment provided by engineer. */ "Remove" = "Удалить"; @@ -1010,9 +1100,15 @@ /* rcv group event chat item */ "removed you" = "удалил(а) вас из группы"; -/* No comment provided by engineer. */ +/* chat item action */ "Reply" = "Ответить"; +/* No comment provided by engineer. */ +"Required" = "Обязательно"; + +/* No comment provided by engineer. */ +"Reset colors" = "Сбросить цвета"; + /* No comment provided by engineer. */ "Reset to defaults" = "Сбросить настройки"; @@ -1026,9 +1122,12 @@ "Revert" = "Отменить изменения"; /* No comment provided by engineer. */ -"Run chat" = "Запустить chat"; +"Risks and limitations:" = "Риски и ограничения:"; /* No comment provided by engineer. */ +"Run chat" = "Запустить chat"; + +/* chat item action */ "Save" = "Сохранить"; /* No comment provided by engineer. */ @@ -1049,6 +1148,9 @@ /* No comment provided by engineer. */ "Scan QR code" = "Сканировать QR код"; +/* No comment provided by engineer. */ +"Search" = "Поиск"; + /* network option */ "sec" = "сек"; @@ -1079,7 +1181,7 @@ /* No comment provided by engineer. */ "Settings" = "Настройки"; -/* No comment provided by engineer. */ +/* chat item action */ "Share" = "Поделиться"; /* No comment provided by engineer. */ @@ -1112,6 +1214,9 @@ /* No comment provided by engineer. */ "SMP servers (one per line)" = "SMP серверы (один на строке)"; +/* No comment provided by engineer. */ +"Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля."; + /* notification title */ "Somebody" = "Контакт"; @@ -1148,6 +1253,9 @@ /* No comment provided by engineer. */ "Tap to join" = "Нажмите, чтобы вступить"; +/* No comment provided by engineer. */ +"Tap to join incognito" = "Нажмите, чтобы вступить инкогнито"; + /* No comment provided by engineer. */ "TCP connection timeout" = "Таймаут TCP соединения"; @@ -1172,12 +1280,18 @@ /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!"; +/* No comment provided by engineer. */ +"The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля."; + /* No comment provided by engineer. */ "The contact you shared this link with will NOT be able to connect!" = "Контакт, которому вы отправили эту ссылку, не сможет соединиться!"; /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "Созданный архив доступен через Настройки приложения."; +/* No comment provided by engineer. */ +"the group is created in Incognito mode," = "группа создана в режиме Инкогнито,"; + /* No comment provided by engineer. */ "The group is fully decentralized – it is visible only to the members." = "Группа полностью децентрализована — она видна только членам."; @@ -1196,6 +1310,9 @@ /* No comment provided by engineer. */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; +/* No comment provided by engineer. */ +"There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown." = "Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение."; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; @@ -1211,6 +1328,9 @@ /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Чтобы задать вопросы и получать уведомления о новых версиях,"; +/* No comment provided by engineer. */ +"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата."; + /* No comment provided by engineer. */ "To make a new connection" = "Чтобы соединиться"; @@ -1259,6 +1379,12 @@ /* authentication reason */ "Unlock" = "Разблокировать"; +/* No comment provided by engineer. */ +"Unmute" = "Уведомлять"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Обновить настройки .onion хостов?"; + /* No comment provided by engineer. */ "Update network settings?" = "Обновить настройки сети?"; @@ -1266,7 +1392,13 @@ "updated group profile" = "обновил(а) профиль группы"; /* No comment provided by engineer. */ -"Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к переподключению клиента ко всем серверам."; +"Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами."; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Использовать .onion хосты"; /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; @@ -1274,6 +1406,9 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = "Для использования .onion хостов требуется совместимый VPN провайдер."; + /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Используются серверы, предоставленные SimpleX Chat."; @@ -1289,6 +1424,9 @@ /* No comment provided by engineer. */ "via relay" = "через relay сервер"; +/* No comment provided by engineer. */ +"Video call" = "Видеозвонок"; + /* No comment provided by engineer. */ "video call (not e2e encrypted)" = "видеозвонок (не e2e зашифрованный)"; @@ -1310,6 +1448,12 @@ /* No comment provided by engineer. */ "Welcome %@!" = "Здравствуйте %@!"; +/* No comment provided by engineer. */ +"When available" = "Когда возможно"; + +/* No comment provided by engineer. */ +"When you join a group incognito, your new member profile is created and shared with all members." = "Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы."; + /* No comment provided by engineer. */ "You" = "Вы"; @@ -1329,7 +1473,10 @@ "You are invited to group" = "Вы приглашены в группу"; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button" = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**."; +"you are invited to group incognito" = "вы приглашены в группу инкогнито"; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**."; /* notification body */ "You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@"; @@ -1352,6 +1499,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; +/* No comment provided by engineer. */ +"you have an incognito connection with the member who invited you." = "вы соединены инкогнито с членом группы, который вас пригласил."; + /* No comment provided by engineer. */ "You invited your contact" = "Вы пригласили ваш контакт"; @@ -1367,6 +1517,9 @@ /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, инача вы можете перестать получать сообщения от каких то контактов."; +/* No comment provided by engineer. */ +"you or the member who invited you joined in Incognito mode," = "вы или пригласивший вас член группы вступили в группу в режиме Инкогнито,"; + /* No comment provided by engineer. */ "You rejected group invitation" = "Вы отклонили приглашение в группу"; @@ -1376,9 +1529,15 @@ /* No comment provided by engineer. */ "You sent group invitation" = "Вы отправили приглашение в группу"; +/* No comment provided by engineer. */ +"You sent group invitation incognito" = "Вы отправили приглашение в группу инкогнито"; + /* chat list item description */ "you shared one-time link" = "вы создали ссылку"; +/* chat list item description */ +"you shared one-time link incognito" = "вы создали ссылку инкогнито"; + /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!"; @@ -1391,9 +1550,18 @@ /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена."; +/* No comment provided by engineer. */ +"You will use a random profile for this group" = "Вы будете использовать случайный профиль для этой группы"; + /* No comment provided by engineer. */ "you: " = "вы: "; +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль"; + +/* No comment provided by engineer. */ +"Your are incognito in a group when:" = "Вы инкогнито в группе, когда:"; + /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; @@ -1406,6 +1574,9 @@ /* No comment provided by engineer. */ "Your chat profile" = "Ваш профиль"; +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен участникам группы"; + /* No comment provided by engineer. */ "Your chat profile will be sent to your contact" = "Ваш профиль будет отправлен вашему контакту"; @@ -1413,7 +1584,7 @@ "Your chats" = "Ваши чаты"; /* No comment provided by engineer. */ -"Your contact can scan it from the app" = "Ваш контакт может сосканировать QR в приложении"; +"Your contact can scan it from the app." = "Ваш контакт может сосканировать QR код в приложении"; /* No comment provided by engineer. */ "Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой)."; @@ -1424,6 +1595,9 @@ /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; +/* No comment provided by engineer. */ +"Your main profile may be shared" = "Вашим основным профилем могут поделиться"; + /* No comment provided by engineer. */ "Your privacy" = "Конфиденциальность"; @@ -1436,6 +1610,9 @@ /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на вашем устройстве."; +/* No comment provided by engineer. */ +"Your random profile" = "Ваш случайный профиль"; + /* No comment provided by engineer. */ "Your settings" = "Настройки"; diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 87ce90f62c..f5345096ba 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -759,9 +759,6 @@ data Connection = Connection aConnId :: Connection -> ConnId aConnId Connection {agentConnId = AgentConnId cId} = cId -connCustomUserProfileId :: Connection -> Maybe Int64 -connCustomUserProfileId Connection {customUserProfileId} = customUserProfileId - instance ToJSON Connection where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} From 53a71cf28c79b6027d60ff99e32f768d3e865f6c Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 24 Aug 2022 19:03:43 +0400 Subject: [PATCH 52/89] core: contact aliases (#968) --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 21 ++- src/Simplex/Chat/Controller.hs | 4 +- .../M20220824_profiles_local_alias.hs | 17 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store.hs | 130 ++++++++++-------- src/Simplex/Chat/Types.hs | 16 ++- src/Simplex/Chat/View.hs | 20 ++- tests/ChatTests.hs | 8 ++ tests/MobileTests.hs | 4 +- 10 files changed, 147 insertions(+), 77 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 63dba222a4..662df81c05 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -47,6 +47,7 @@ library Simplex.Chat.Migrations.M20220818_chat_notifications Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items + Simplex.Chat.Migrations.M20220824_profiles_local_alias Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 78a6b1c113..5fd93ec61b 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -578,6 +578,11 @@ processChatCommand = \case withCurrentCall contactId $ \userId ct call -> updateCallItemStatus userId ct call receivedStatus Nothing $> Just call APIUpdateProfile profile -> withUser (`updateProfile` profile) + APISetContactAlias contactId localAlias -> withUser $ \User {userId} -> do + ct' <- withStore $ \db -> do + ct <- getContact db userId contactId + liftIO $ updateContactAlias db userId ct localAlias + pure $ CRContactAliasUpdated ct' APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> CRNtfTokenStatus <$> withUser (\_ -> withAgent $ \a -> registerNtfToken a token mode) @@ -619,7 +624,7 @@ processChatCommand = \case ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db userId contactId incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- withAgent (`getConnectionServers` contactConnId ct) - pure $ CRContactInfo ct connectionStats incognitoProfile + pure $ CRContactInfo ct connectionStats (fmap fromLocalProfile incognitoProfile) APIGroupMemberInfo gId gMemberId -> withUser $ \user@User {userId} -> do -- [incognito] print group member main profile (g, m@GroupMember {memberContactProfileId}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId @@ -984,11 +989,11 @@ processChatCommand = \case unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f (,) <$> getFileSize fsFilePath <*> asks (fileChunkSize . config) updateProfile :: User -> Profile -> m ChatResponse - updateProfile user@User {profile = p@LocalProfile {profileId}} p'@Profile {displayName} + updateProfile user@User {profile = p@LocalProfile {profileId, localAlias}} p'@Profile {displayName} | p' == fromLocalProfile p = pure CRUserProfileNoChange | otherwise = do withStore $ \db -> updateUserProfile db user p' - let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p'} + let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p' localAlias} asks currentUser >>= atomically . (`writeTVar` Just user') -- [incognito] filter out contacts with whom user has incognito connections contacts <- @@ -1365,7 +1370,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage CONF confId _ connInfo -> do -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile + let profileToSend = fromLocalProfile $ fromMaybe profile incognitoProfile saveConnInfo conn connInfo allowAgentConnection conn confId $ XInfo profileToSend INFO connInfo -> @@ -1430,7 +1435,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage Nothing -> do -- [incognito] print incognito profile used for this contact incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - toView $ CRContactConnected ct incognitoProfile + toView $ CRContactConnected ct (fmap fromLocalProfile incognitoProfile) setActive $ ActiveC c showToast (c <> "> ") "connected" forM_ viaUserContactLink $ \userContactLinkId -> do @@ -1512,14 +1517,14 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage case memberCategory m of GCHostMember -> do -- [incognito] chat item & event with indication that host connected incognito - mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + mainProfile <- fmap fromLocalProfile <$> if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing memberConnectedChatItem gInfo m mainProfile toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} (memberIncognito membership) setActive $ ActiveG gName showToast ("#" <> gName) "you are connected to group" GCInviteeMember -> do -- [incognito] chat item & event with indication that invitee connected incognito - mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + mainProfile <- fmap fromLocalProfile <$> if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing memberConnectedChatItem gInfo m mainProfile toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} mainProfile setActive $ ActiveG gName @@ -2575,6 +2580,7 @@ chatCommandP = "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), "/_call get" $> APIGetCallInvitations, "/_profile " *> (APIUpdateProfile <$> jsonP), + "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString), "/_ntf get" $> APIGetNtfToken, "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), @@ -2685,6 +2691,7 @@ chatCommandP = fullNameP name = do n <- (A.space *> A.takeByteString) <|> pure "" pure $ if B.null n then name else safeDecodeUtf8 n + textP = safeDecodeUtf8 <$> A.takeByteString filePath = T.unpack . safeDecodeUtf8 <$> A.takeByteString searchP = T.unpack . safeDecodeUtf8 <$> (" search=" *> A.takeByteString) memberRole = diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 416fa97b53..e1088a050b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -133,6 +133,7 @@ data ChatCommand | APIGetCallInvitations | APICallStatus ContactId WebRTCCallStatus | APIUpdateProfile Profile + | APISetContactAlias ContactId LocalAlias | APIParseMarkdown Text | APIGetNtfToken | APIRegisterToken DeviceToken NotificationsMode @@ -214,7 +215,7 @@ data ChatResponse | CRUserSMPServers {smpServers :: [SMPServer]} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} - | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats, mainProfile :: Maybe Profile} + | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats, localMainProfile :: Maybe LocalProfile} | CRNewChatItem {chatItem :: AChatItem} | CRChatItemStatusUpdated {chatItem :: AChatItem} | CRChatItemUpdated {chatItem :: AChatItem} @@ -267,6 +268,7 @@ data ChatResponse | CRSndFileRcvCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndGroupFileCancelled {chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} | CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile} + | CRContactAliasUpdated {toContact :: Contact} | CRContactConnecting {contact :: Contact} | CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile} | CRContactAnotherClient {contact :: Contact} diff --git a/src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs b/src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs new file mode 100644 index 0000000000..f0b0ca8385 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220824_profiles_local_alias where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220824_profiles_local_alias :: Query +m20220824_profiles_local_alias = + [sql| +PRAGMA ignore_check_constraints=ON; + +ALTER TABLE contact_profiles ADD COLUMN local_alias TEXT DEFAULT '' CHECK (local_alias NOT NULL); +UPDATE contact_profiles SET local_alias = ''; + +PRAGMA ignore_check_constraints=OFF; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 4807813c4b..55bd57cec5 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -14,7 +14,8 @@ CREATE TABLE contact_profiles( updated_at TEXT CHECK(updated_at NOT NULL), image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE, - incognito INTEGER + incognito INTEGER, + local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL) ); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 3b3520437b..198e2e2d0a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -38,6 +38,7 @@ module Simplex.Chat.Store getContactIdByName, updateUserProfile, updateContactProfile, + updateContactAlias, getUserContacts, createUserContactLink, getUserContactLinkConnections, @@ -232,6 +233,7 @@ import Simplex.Chat.Migrations.M20220812_incognito_profiles import Simplex.Chat.Migrations.M20220818_chat_notifications import Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id import Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items +import Simplex.Chat.Migrations.M20220824_profiles_local_alias import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -265,7 +267,8 @@ schemaMigrations = ("20220812_incognito_profiles", m20220812_incognito_profiles), ("20220818_chat_notifications", m20220818_chat_notifications), ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id), - ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items) + ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items), + ("20220824_profiles_local_alias", m20220824_profiles_local_alias) ] -- | The list of migrations in ascending order by date @@ -331,7 +334,7 @@ getUsers db = toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData) -> User toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image) = - let profile = LocalProfile {profileId, displayName, fullName, image} + let profile = LocalProfile {profileId, displayName, fullName, image, localAlias = ""} in User {userId, userContactId, localDisplayName = displayName, profile, activeUser} setActiveUser :: DB.Connection -> UserId -> IO () @@ -370,7 +373,7 @@ getConnReqContactXContactId db userId cReqHash = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -416,20 +419,20 @@ createIncognitoProfile_ db userId createdAt incognitoProfile = (displayName, fullName, image, userId, Just True, createdAt, createdAt) insertedRowId db -getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO Profile +getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = ExceptT . firstRow toProfile (SEProfileNotFound profileId) $ DB.query db [sql| - SELECT display_name, full_name, image + SELECT display_name, full_name, image, local_alias FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? |] (userId, profileId) where - toProfile :: (ContactName, Text, Maybe ImageData) -> Profile - toProfile (displayName, fullName, image) = Profile {displayName, fullName, image} + toProfile :: (ContactName, Text, Maybe ImageData, LocalAlias) -> LocalProfile + toProfile (displayName, fullName, image, localAlias) = LocalProfile {profileId, displayName, fullName, image, localAlias} createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do @@ -453,7 +456,7 @@ createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> Excep createDirectContact db userId activeConn@Connection {connId} profile = do createdAt <- liftIO getCurrentTime (localDisplayName, contactId, profileId) <- createContact_ db userId connId profile Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt} createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs = @@ -536,15 +539,28 @@ updateUserProfile db User {userId, userContactId, localDisplayName, profile = Lo updateContact_ db userId userContactId localDisplayName newName currentTs updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact -updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName}} p'@Profile {displayName = newName} +updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} p'@Profile {displayName = newName} | displayName == newName = - liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p'} + liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p' localAlias} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime updateContactProfile_' db userId profileId p' currentTs updateContact_ db userId contactId localDisplayName ldn currentTs - pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p'} + pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias} + +updateContactAlias :: DB.Connection -> UserId -> Contact -> LocalAlias -> IO Contact +updateContactAlias db userId c@Contact {profile = lp@LocalProfile {profileId}} localAlias = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE contact_profiles + SET local_alias = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + |] + (localAlias, updatedAt, userId, profileId) + pure $ (c :: Contact) {profile = lp {localAlias = localAlias}} updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do @@ -574,18 +590,18 @@ updateContact_ db userId contactId displayName newName updatedAt = do (newName, updatedAt, userId, contactId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId) -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, LocalAlias, Maybe Bool, UTCTime, UTCTime) toContact :: ContactRow :. ConnectionRow -> Contact -toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image} +toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, enableNtfs_, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, localAlias} activeConn = toConnection connRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image} +toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, enableNtfs_, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in case toMaybeConnection connRow of Just activeConn -> @@ -769,7 +785,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -892,7 +908,7 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile u (userId, localDisplayName, profileId, True, createdAt, createdAt, xContactId) contactId <- insertedRowId db activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -1163,15 +1179,15 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.enable_ntfs, c.created_at, c.updated_at + SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, c.via_group, c.enable_ntfs, c.created_at, c.updated_at FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt)] = - let profile = LocalProfile {profileId, displayName, fullName, image} + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, enableNtfs_, createdAt, updatedAt)] = + let profile = LocalProfile {profileId, displayName, fullName, image, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" @@ -1188,10 +1204,10 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, + pu.display_name, pu.full_name, pu.image, pu.local_alias, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -1276,10 +1292,10 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, + pu.display_name, pu.full_name, pu.image, pu.local_alias, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m @@ -1367,10 +1383,10 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do - customUserProfileId <- liftIO $ createIncognitoProfile_ db userId createdAt incognitoProfile - (localDisplayName, memberProfile) <- case (incognitoProfile, customUserProfileId) of + incognitoProfileId <- liftIO $ createIncognitoProfile_ db userId createdAt incognitoProfile + (localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of (Just profile@Profile {displayName}, Just profileId) -> - (,toLocalProfile profileId profile) <$> insertMemberIncognitoProfile_ displayName profileId + (,toLocalProfile profileId profile "") <$> insertMemberIncognitoProfile_ displayName profileId _ -> (,profile' userOrContact) <$> liftIO insertMember_ groupMemberId <- liftIO $ insertedRowId db pure @@ -1476,7 +1492,7 @@ getUserGroupDetails db User {userId, userContactId} = [sql| SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at, mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, - mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image + mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.local_alias FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -1506,7 +1522,7 @@ getGroupMember db user@User {userId} groupId groupMemberId = [sql| SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m @@ -1528,7 +1544,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do [sql| SELECT m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m @@ -1566,20 +1582,20 @@ getGroupInvitation db user groupId = do findFromContact (IBContact contactId) = find ((== Just contactId) . memberContactId) findFromContact _ = const Nothing -type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData)) +type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, LocalAlias)) -type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData)) +type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe LocalAlias)) toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image)) = - let memberProfile = LocalProfile {profileId, displayName, fullName, image} +toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias)) = + let memberProfile = LocalProfile {profileId, displayName, fullName, image, localAlias} invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing in GroupMember {..} toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, Just localAlias)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias)) toMaybeGroupMember _ _ = Nothing createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember @@ -1647,8 +1663,8 @@ updateGroupMemberStatus db userId GroupMember {groupMemberId} memStatus = do createMemberIncognitoProfile :: DB.Connection -> UserId -> GroupMember -> Maybe Profile -> ExceptT StoreError IO GroupMember createMemberIncognitoProfile db userId m@GroupMember {groupMemberId} incognitoProfile = do currentTs <- liftIO getCurrentTime - customUserProfileId <- liftIO $ createIncognitoProfile_ db userId currentTs incognitoProfile - case (incognitoProfile, customUserProfileId) of + incognitoProfileId <- liftIO $ createIncognitoProfile_ db userId currentTs incognitoProfile + case (incognitoProfile, incognitoProfileId) of (Just profile@Profile {displayName}, Just profileId) -> ExceptT $ withLocalDisplayName db userId displayName $ \incognitoLdn -> do @@ -1660,7 +1676,7 @@ createMemberIncognitoProfile db userId m@GroupMember {groupMemberId} incognitoPr WHERE user_id = ? AND group_member_id = ? |] (incognitoLdn, profileId, currentTs, userId, groupMemberId) - pure . Right $ m {localDisplayName = incognitoLdn, memberProfile = toLocalProfile profileId profile} + pure . Right $ m {localDisplayName = incognitoLdn, memberProfile = toLocalProfile profileId profile ""} _ -> pure m -- | add new member with profile @@ -1712,7 +1728,7 @@ createNewMember_ |] (groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) groupMemberId <- insertedRowId db - pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile, memberContactId, memberContactProfileId, activeConn} + pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn} deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId} = do @@ -1901,10 +1917,10 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, + pu.display_name, pu.full_name, pu.image, pu.local_alias, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, - m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM group_members m @@ -1936,7 +1952,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = db [sql| SELECT - ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct @@ -1952,9 +1968,9 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact - toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image} + toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact + toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} activeConn = toConnection connRow in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} @@ -2658,7 +2674,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image + p.display_name, p.full_name, p.image, p.local_alias FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -2695,7 +2711,7 @@ getDirectChatPreviews_ db User {userId} = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, @@ -2764,7 +2780,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, + pu.display_name, pu.full_name, pu.image, pu.local_alias, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), -- ChatItem @@ -2774,13 +2790,13 @@ getGroupChatPreviews_ db User {userId, userContactId} = do -- Maybe GroupMember - sender m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image, + p.display_name, p.full_name, p.image, p.local_alias, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.image + rp.display_name, rp.full_name, rp.image, rp.local_alias FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -3035,7 +3051,7 @@ getContact db userId contactId = [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -3164,7 +3180,7 @@ getGroupInfo db User {userId, userContactId} groupId = -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image + pu.display_name, pu.full_name, pu.image, pu.local_alias FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -3533,13 +3549,13 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image, + p.display_name, p.full_name, p.image, p.local_alias, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category, rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.image + rp.display_name, rp.full_name, rp.image, rp.local_alias FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index f5345096ba..93612bd6a6 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -218,7 +218,10 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, image :: Maybe ImageData - -- incognito field should not be read as is into this data type to prevent sending it as part of profile to contacts + -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: + -- - contact_profile_id + -- - incognito + -- - local_alias } deriving (Eq, Show, Generic, FromJSON) @@ -226,11 +229,14 @@ instance ToJSON Profile where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +type LocalAlias = Text + data LocalProfile = LocalProfile { profileId :: ProfileId, displayName :: ContactName, fullName :: Text, - image :: Maybe ImageData + image :: Maybe ImageData, + localAlias :: LocalAlias } deriving (Eq, Show, Generic, FromJSON) @@ -241,9 +247,9 @@ instance ToJSON LocalProfile where localProfileId :: LocalProfile -> ProfileId localProfileId = profileId -toLocalProfile :: ProfileId -> Profile -> LocalProfile -toLocalProfile profileId Profile {displayName, fullName, image} = - LocalProfile {profileId, displayName, fullName, image} +toLocalProfile :: ProfileId -> Profile -> LocalAlias -> LocalProfile +toLocalProfile profileId Profile {displayName, fullName, image} localAlias = + LocalProfile {profileId, displayName, fullName, image, localAlias} fromLocalProfile :: LocalProfile -> Profile fromLocalProfile LocalProfile {displayName, fullName, image} = diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 443ce285fe..31b159af18 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -117,6 +117,7 @@ responseToView testView = \case CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft CRUserProfileUpdated p p' -> viewUserProfileUpdated p p' + CRContactAliasUpdated c -> viewContactAliasUpdated c CRContactUpdated c c' -> viewContactUpdated c c' CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile @@ -594,23 +595,29 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = ] viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo Contact {contactId} stats incognitoProfile = +viewContactInfo Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewConnectionStats stats <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) incognitoProfile + <> if localAlias /= "" then ["alias: " <> plain localAlias] else ["alias not set"] -viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId} stats mainProfile = +viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> Maybe LocalProfile -> [StyledString] +viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias = mpLocalAlias}} stats mainProfile = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats <> maybe ["unknown whether group member uses his main profile or incognito one for the group"] - (\Profile {displayName, fullName} -> ["member is using " <> styleIncognito' "incognito" <> " profile for the group, main profile known: " <> ttyFullName displayName fullName]) + (\LocalProfile {displayName, fullName} -> ["member is using " <> styleIncognito' "incognito" <> " profile for the group, main profile known: " <> ttyFullName displayName fullName]) mainProfile + <> if alias /= "" then ["alias: " <> plain alias] else ["no alias for contact"] + where + alias = case mainProfile of + Nothing -> mpLocalAlias + Just LocalProfile {localAlias = lpLocalAlias} -> lpLocalAlias viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvServers, sndServers} = @@ -644,6 +651,11 @@ viewGroupUpdated where byMember = maybe "" ((" by " <>) . ttyMember) m +viewContactAliasUpdated :: Contact -> [StyledString] +viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}} + | localAlias == "" = ["contact " <> ttyContact n <> " alias removed"] + | otherwise = ["contact " <> ttyContact n <> " alias updated: " <> plain localAlias] + viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated Contact {localDisplayName = n, profile = LocalProfile {fullName}} diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 4d60e6538d..06afb1595e 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -95,6 +95,7 @@ chatTests = do it "create group incognito" testCreateGroupIncognito it "join group incognito" testJoinGroupIncognito it "can't invite contact to whom user connected incognito to non incognito group" testCantInviteIncognitoConnectionNonIncognitoGroup + it "set contact alias" testSetAlias describe "SMP servers" $ it "get and set SMP servers" testGetSetSMPServers describe "async connection handshake" $ do @@ -2440,6 +2441,13 @@ testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobP alice ##> "/a club bob" alice <## "you're using main profile for this group - prohibited to invite contact to whom you are connected incognito" +testSetAlias :: IO () +testSetAlias = testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice #$> ("/_set alias @2 my friend bob", id, "contact bob alias updated: my friend bob") + alice #$> ("/_set alias @2", id, "contact bob alias removed") + testGetSetSMPServers :: IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index c137af6710..68057b5dc4 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -31,9 +31,9 @@ activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\" activeUser :: String #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}}" +activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}}" #else -activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}" +activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}" #endif chatStarted :: String From a5e74ea2f0545e201af1a24a832eecfd08caef2b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 24 Aug 2022 23:15:26 +0300 Subject: [PATCH 53/89] android: choosing theme and accent color (#967) * Theme selector - ability to select from three default choices: system theme, light, dark - ability to choose color accent (primary color) * Removed unused code and made small changes to colors and buttons --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 8 +- .../java/chat/simplex/app/ui/theme/Theme.kt | 39 ++++-- .../chat/simplex/app/ui/theme/ThemeManager.kt | 64 ++++++++++ .../app/views/call/IncomingCallAlertView.kt | 2 +- .../simplex/app/views/chat/ChatInfoView.kt | 2 +- .../chat/simplex/app/views/chat/ChatView.kt | 6 +- .../simplex/app/views/chat/ComposeFileView.kt | 3 +- .../views/chat/group/AddGroupMembersView.kt | 2 +- .../app/views/chat/group/GroupChatInfoView.kt | 4 +- .../views/chat/group/GroupMemberInfoView.kt | 2 +- .../simplex/app/views/chat/item/CIFileView.kt | 5 +- .../views/chat/item/CIGroupInvitationView.kt | 3 +- .../simplex/app/views/chat/item/CIMetaView.kt | 6 +- .../app/views/chat/item/FramedItemView.kt | 2 +- .../app/views/chatlist/ChatPreviewView.kt | 3 +- .../views/chatlist/ContactConnectionView.kt | 3 +- .../app/views/chatlist/ContactRequestView.kt | 3 +- .../simplex/app/views/helpers/AlertManager.kt | 5 +- .../app/views/helpers/DefaultTopAppBar.kt | 5 +- .../chat/simplex/app/views/helpers/Section.kt | 31 ++++- .../simplex/app/views/newchat/AddGroupView.kt | 2 +- .../app/views/onboarding/SimpleXInfo.kt | 3 +- .../app/views/usersettings/Appearance.kt | 117 ++++++++++++++++-- .../app/views/usersettings/SettingsView.kt | 24 +--- .../app/views/usersettings/ThemeSelector.kt | 69 +++++++++++ .../app/src/main/res/values-ru/strings.xml | 14 ++- .../app/src/main/res/values/strings.xml | 14 ++- 28 files changed, 358 insertions(+), 84 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 4765f58c5e..4fc7791a26 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation "androidx.navigation:navigation-compose:2.4.1" implementation "com.google.accompanist:accompanist-insets:0.23.0" implementation 'androidx.webkit:webkit:1.4.0' + implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2" def work_version = "2.7.1" implementation "androidx.work:work-runtime-ktx:$work_version" diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 2166882959..44f4e7f7c6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -5,7 +5,6 @@ import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application import android.content.* -import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.PowerManager @@ -16,12 +15,14 @@ import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Bolt import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import chat.simplex.app.* import chat.simplex.app.R +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage @@ -95,6 +96,9 @@ class AppPreferences(val context: Context) { val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl) val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) + val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) + val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb()) + private fun mkIntPreference(prefName: String, default: Int) = Preference( get = fun() = sharedPreferences.getInt(prefName, default), @@ -161,6 +165,8 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_NETWORK_TCP_KEEP_IDLE = "NetworkTCPKeepIdle" private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" + private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" + private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor" } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index a7ee4c9fb7..a475beed7c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -2,10 +2,15 @@ package chat.simplex.app.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.flow.MutableStateFlow -private val DarkColorPalette = darkColors( +enum class DefaultTheme { + SYSTEM, DARK, LIGHT +} + +val DarkColorPalette = darkColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primaryVariant = SimplexGreen, secondary = DarkGray, @@ -18,7 +23,7 @@ private val DarkColorPalette = darkColors( onSurface = Color(0xFFFFFBFA), // onError: Color = Color.Black, ) -private val LightColorPalette = lightColors( +val LightColorPalette = lightColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primaryVariant = SimplexGreen, secondary = LightGray, @@ -30,16 +35,28 @@ private val LightColorPalette = lightColors( // onSurface = Color.Black, ) -@Composable -fun SimpleXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette - } +val CurrentColors: MutableStateFlow> = MutableStateFlow(ThemeManager.currentColors(true)) +@Composable +fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.first.isLight + +@Composable +fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { + LaunchedEffect(darkTheme) { + // For preview + if (darkTheme != null) + CurrentColors.value = ThemeManager.currentColors(darkTheme) + } + val systemDark = isSystemInDarkTheme() + LaunchedEffect(systemDark) { + if (CurrentColors.value.second == DefaultTheme.SYSTEM && CurrentColors.value.first.isLight == systemDark) { + // Change active colors from light to dark and back based on system theme + ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark) + } + } + val theme by CurrentColors.collectAsState() MaterialTheme( - colors = colors, + colors = theme.first, typography = Typography, shapes = Shapes, content = content diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt new file mode 100644 index 0000000000..a1d5ee4526 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt @@ -0,0 +1,64 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.material.Colors +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import chat.simplex.app.R +import chat.simplex.app.SimplexApp +import chat.simplex.app.model.AppPreferences +import chat.simplex.app.views.helpers.generalGetString + +object ThemeManager { + private val appPrefs: AppPreferences by lazy { + AppPreferences(SimplexApp.context) + } + + fun currentColors(darkForSystemTheme: Boolean): Pair { + val theme = appPrefs.currentTheme.get()!! + val systemThemeColors = if (darkForSystemTheme) DarkColorPalette else LightColorPalette + val res = when (theme) { + DefaultTheme.SYSTEM.name -> Pair(systemThemeColors, DefaultTheme.SYSTEM) + DefaultTheme.DARK.name -> Pair(DarkColorPalette, DefaultTheme.DARK) + DefaultTheme.LIGHT.name -> Pair(LightColorPalette, DefaultTheme.LIGHT) + else -> Pair(systemThemeColors, DefaultTheme.SYSTEM) + } + return res.copy(first = res.first.copy(primary = Color(appPrefs.primaryColor.get()))) + } + + // colors, default theme enum, localized name of theme + fun allThemes(darkForSystemTheme: Boolean): List> { + val allThemes = ArrayList>() + allThemes.add( + Triple( + if (darkForSystemTheme) DarkColorPalette else LightColorPalette, + DefaultTheme.SYSTEM, + generalGetString(R.string.theme_system) + ) + ) + allThemes.add( + Triple( + LightColorPalette, + DefaultTheme.LIGHT, + generalGetString(R.string.theme_light) + ) + ) + allThemes.add( + Triple( + DarkColorPalette, + DefaultTheme.DARK, + generalGetString(R.string.theme_dark) + ) + ) + return allThemes + } + + fun applyTheme(name: String, darkForSystemTheme: Boolean) { + appPrefs.currentTheme.set(name) + CurrentColors.value = currentColors(darkForSystemTheme) + } + + fun saveAndApplyPrimaryColor(color: Color) { + appPrefs.primaryColor.set(color.toArgb()) + CurrentColors.value = currentColors(!CurrentColors.value.first.isLight) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt index 631649211c..31e99eda96 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt @@ -45,7 +45,7 @@ fun IncomingCallAlertLayout( ignoreCall: () -> Unit, acceptCall: () -> Unit ) { - val color = if (isSystemInDarkTheme()) IncomingCallDark else IncomingCallLight + val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight Column(Modifier.background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { IncomingCallInfo(invitation) Spacer(Modifier.height(8.dp)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 72c656261c..1734a3f1b5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -198,7 +198,7 @@ fun ChatInfoHeader(cInfo: ChatInfo) { Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight) + ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) Text( cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 095f3aec15..f56e5e90f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -114,7 +114,7 @@ fun ChatView(chatModel: ChatModel) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { ChatInfoView(chatModel, connStats, close) } @@ -124,7 +124,7 @@ fun ChatView(chatModel: ChatModel) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { GroupChatInfoView(chatModel, close) } @@ -189,7 +189,7 @@ fun ChatView(chatModel: ChatModel) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { AddGroupMembersView(groupInfo, chatModel, close) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt index 48917ec05e..dd7b628e1e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt @@ -1,5 +1,4 @@ import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -31,7 +30,7 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo Modifier .padding(start = 4.dp, end = 2.dp) .size(36.dp), - tint = if (isSystemInDarkTheme()) FileDark else FileLight + tint = if (isInDarkTheme()) FileDark else FileLight ) Text(fileName) Spacer(Modifier.weight(1f)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index b88c0b5470..6730da6fb3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -93,7 +93,7 @@ fun AddGroupMembersLayout( ChatInfoToolbarTitle( ChatInfo.Group(groupInfo), imageSize = 60.dp, - iconColor = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight + iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight ) } SectionSpacer() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index d2d7787718..3c827752c5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -47,7 +47,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { AddGroupMembersView(groupInfo, chatModel, close) } @@ -60,7 +60,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { GroupMemberInfoView(groupInfo, member, connStats, chatModel, close) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 6a0209cc1e..ba0a39bd3f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -139,7 +139,7 @@ fun GroupMemberInfoHeader(member: GroupMember) { Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ProfileImage(size = 192.dp, member.image, color = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight) + ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) Text( member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt index fdf25baba9..dd5992600d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt @@ -2,7 +2,6 @@ package chat.simplex.app.views.chat.item import android.widget.Toast import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -39,7 +38,7 @@ fun CIFileView( @Composable fun fileIcon( innerIcon: ImageVector? = null, - color: Color = if (isSystemInDarkTheme()) FileDark else FileLight + color: Color = if (isInDarkTheme()) FileDark else FileLight ) { Box( contentAlignment = Alignment.Center @@ -105,7 +104,7 @@ fun CIFileView( fun progressIndicator() { CircularProgressIndicator( Modifier.size(32.dp), - color = if (isSystemInDarkTheme()) FileDark else FileLight, + color = if (isInDarkTheme()) FileDark else FileLight, strokeWidth = 4.dp ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt index 03db38ba4b..73e7608d2b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt @@ -3,7 +3,6 @@ package chat.simplex.app.views.chat.item import android.content.res.Configuration import android.util.Log import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -39,7 +38,7 @@ fun CIGroupInvitationView( val p = groupInvitation.groupProfile val iconColor = if (action) MaterialTheme.colors.primary - else if (isSystemInDarkTheme()) FileDark else FileLight + else if (isInDarkTheme()) FileDark else FileLight Row( Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index 126b477e80..f5566b6f19 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -1,8 +1,7 @@ package chat.simplex.app.views.chat.item import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.runtime.Composable @@ -16,7 +15,6 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimplexBlue import kotlinx.datetime.Clock @Composable @@ -56,7 +54,7 @@ fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) { Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = Color.Yellow) } is CIStatus.RcvNew -> { - Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = SimplexBlue) + Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary) } else -> {} } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt index ff69501d0c..d74392946d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt @@ -87,7 +87,7 @@ fun FramedItemView( Modifier .padding(top = 6.dp, end = 4.dp) .size(22.dp), - tint = if (isSystemInDarkTheme()) FileDark else FileLight + tint = if (isInDarkTheme()) FileDark else FileLight ) } else -> ciQuotedMsgView(qi) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 5bb8e8526f..dc71336cef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -2,7 +2,6 @@ package chat.simplex.app.views.chatlist import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -88,7 +87,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { metaText = ci.timestampText, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body1.copy(color = if (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), + style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), ) } else { when (cInfo) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt index 318f1a0093..2ee852aeb3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -37,7 +36,7 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { fontWeight = FontWeight.Bold, color = HighOrLowlight ) - Text(contactConnection.description, maxLines = 2, color = if (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight) + Text(contactConnection.description, maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } val ts = getTimestampText(contactConnection.updatedAt) Column( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt index 3b90163243..6ec6263f26 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -33,7 +32,7 @@ fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary ) - Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight) + Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } val ts = getTimestampText(contactRequest.contactRequest.updatedAt) Column( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 55715f8e84..f4a6595798 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -2,8 +2,9 @@ package chat.simplex.app.views.helpers import android.util.Log import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import chat.simplex.app.R import chat.simplex.app.TAG diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt index 1e6c37902a..a4962dc7a8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt @@ -13,8 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.ui.theme.ToolbarDark -import chat.simplex.app.ui.theme.ToolbarLight +import chat.simplex.app.ui.theme.* @Composable fun DefaultTopAppBar( @@ -39,7 +38,7 @@ fun DefaultTopAppBar( SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged) } }, - backgroundColor = if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight, + backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight, navigationIcon = navigationButton, buttons = if (!showSearch) buttons else emptyList(), centered = !showSearch diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt index 6a04db96e3..9ec912abcb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt @@ -1,5 +1,4 @@ import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -8,8 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.app.ui.theme.GroupDark -import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.* @Composable fun SectionView(title: String? = null, content: (@Composable () -> Unit)) { @@ -20,7 +18,7 @@ fun SectionView(title: String? = null, content: (@Composable () -> Unit)) { modifier = Modifier.padding(start = 16.dp, bottom = 5.dp), fontSize = 12.sp ) } - Surface(color = if (isSystemInDarkTheme()) GroupDark else MaterialTheme.colors.background) { + Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) { Column(Modifier.padding(horizontal = 6.dp).fillMaxWidth()) { content() } } } @@ -40,6 +38,27 @@ fun SectionItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, disabled: B } } +@Composable +fun SectionItemViewSpaceBetween( + click: (() -> Unit)? = null, + height: Dp = 46.dp, + padding: PaddingValues = PaddingValues(horizontal = 8.dp), + disabled: Boolean = false, + content: (@Composable () -> Unit) +) { + val modifier = Modifier + .padding(padding) + .fillMaxWidth() + .height(height) + Row( + if (click == null || disabled) modifier else modifier.clickable(onClick = click), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + content() + } +} + @Composable fun SectionTextFooter(text: String) { Text( @@ -51,9 +70,9 @@ fun SectionTextFooter(text: String) { } @Composable -fun SectionCustomFooter(content: (@Composable () -> Unit)) { +fun SectionCustomFooter(padding: PaddingValues = PaddingValues(start = 16.dp, end = 16.dp, top = 5.dp), content: (@Composable () -> Unit)) { Row( - Modifier.padding(horizontal = 16.dp).padding(top = 5.dp) + Modifier.padding(padding) ) { content() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt index c4c56d65a8..d4d03bfbc3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt @@ -46,7 +46,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { AddGroupMembersView(groupInfo, chatModel, close) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt index 9b66a14af8..5c95b8a727 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt @@ -3,7 +3,6 @@ package chat.simplex.app.views.onboarding import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.Image -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -77,7 +76,7 @@ fun SimpleXInfoLayout( @Composable fun SimpleXLogo() { Image( - painter = painterResource(if (isSystemInDarkTheme()) R.drawable.logo_light else R.drawable.logo), + painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo), contentDescription = stringResource(R.string.image_descr_simplex_logo), modifier = Modifier .padding(vertical = 20.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt index 8a35d64916..3ab5ac25ab 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt @@ -1,5 +1,8 @@ package chat.simplex.app.views.usersettings +import SectionCustomFooter +import SectionItemViewSpaceBetween +import SectionSpacer import SectionView import android.content.ComponentName import android.content.pm.PackageManager @@ -10,11 +13,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Circle import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -24,7 +29,10 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import chat.simplex.app.* import chat.simplex.app.R +import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* +import com.godaddy.android.colorpicker.* enum class AppIcon(val resId: Int) { DEFAULT(R.mipmap.icon), @@ -32,7 +40,9 @@ enum class AppIcon(val resId: Int) { } @Composable -fun AppearanceView() { +fun AppearanceView( + showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), +) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } fun setAppIcon(newIcon: AppIcon) { @@ -54,18 +64,33 @@ fun AppearanceView() { AppearanceLayout( appIcon, - changeIcon = ::setAppIcon + changeIcon = ::setAppIcon, + showThemeSelector = showCustomModal { _, close -> + ModalView( + close = close, modifier = Modifier, + background = if (isInDarkTheme()) colors.background else SettingsBackgroundLight + ) { ThemeSelectorView() } + }, + editPrimaryColor = { primary -> + showCustomModal { _, close -> + ModalView( + close = close, modifier = Modifier, + background = if (isInDarkTheme()) colors.background else SettingsBackgroundLight + ) { ColorEditor(primary, close) } + }() + }, ) } @Composable fun AppearanceLayout( icon: MutableState, - changeIcon: (AppIcon) -> Unit + changeIcon: (AppIcon) -> Unit, + showThemeSelector: () -> Unit, + editPrimaryColor: (Color) -> Unit, ) { Column( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( stringResource(R.string.appearance_settings), @@ -73,10 +98,7 @@ fun AppearanceView() { style = MaterialTheme.typography.h1 ) SectionView(stringResource(R.string.settings_section_title_icon)) { - LazyRow( - Modifier - .padding(horizontal = 8.dp) - ) { + LazyRow { items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> val item = AppIcon.values()[index] val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!! @@ -97,9 +119,82 @@ fun AppearanceView() { } } } + + SectionSpacer() + val currentTheme by CurrentColors.collectAsState() + SectionView(stringResource(R.string.settings_section_title_themes)) { + Column( + Modifier.padding(horizontal = 8.dp) + ) { + SectionItemViewSpaceBetween(showThemeSelector, padding = PaddingValues()) { + Text(generalGetString(R.string.theme)) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + + SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }, padding = PaddingValues()) { + val title = generalGetString(R.string.color_primary) + Text(title) + Icon(Icons.Filled.Circle, title, tint = colors.primary) + } + } + } + if (currentTheme.first.primary != LightColorPalette.primary) { + SectionCustomFooter(PaddingValues(start = 7.dp, end = 7.dp, top = 5.dp)) { + TextButton( + onClick = { + ThemeManager.saveAndApplyPrimaryColor(LightColorPalette.primary) + }, + ) { + Text(generalGetString(R.string.reset_color)) + } + } + } } } +@Composable +fun ColorEditor( + initialColor: Color, + close: () -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + ) { + var currentColor by remember { mutableStateOf(initialColor) } + ColorPicker(initialColor) { + currentColor = it + } + + SectionSpacer() + + TextButton( + onClick = { + ThemeManager.saveAndApplyPrimaryColor(currentColor) + close() + }, + Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.textButtonColors(contentColor = currentColor) + ) { + Text(generalGetString(R.string.save_color)) + } + } +} + +@Composable +fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { + ClassicColorPicker( + color = initialColor, + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + showAlphaBar = false, + onColorChanged = { color: HsvColor -> + onColorChanged(color.toColor()) + } + ) +} + private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> SimplexApp.context.packageManager.getComponentEnabledSetting( ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") @@ -112,7 +207,9 @@ fun PreviewAppearanceSettings() { SimpleXTheme { AppearanceLayout( icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, - changeIcon = {} + changeIcon = {}, + showThemeSelector = {}, + editPrimaryColor = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 8d5358c52b..9061369acc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -58,24 +58,12 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, showSettingsModal = { modalView -> { ModalManager.shared.showCustomModal { close -> ModalView(close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) { + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) { modalView(chatModel) } } } }, showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } }, showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }, - showAppearance = { - withApi { - ModalManager.shared.showCustomModal { close -> - ModalView( - close = close, modifier = Modifier, - background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight - ) { - AppearanceView() - } - } - } - } // showVideoChatPrototype = { ModalManager.shared.showCustomModal { close -> CallViewDebug(close) } }, ) } @@ -88,7 +76,7 @@ val simplexTeamUri = //fun showSectionedModal(chatModel: ChatModel, modalView: (@Composable (ChatModel) -> Unit)) { // ModalManager.shared.showCustomModal { close -> // ModalView(close = close, modifier = Modifier, -// background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) { +// background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) { // modalView(chatModel) // } // } @@ -106,7 +94,6 @@ fun SettingsLayout( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showTerminal: () -> Unit, - showAppearance: () -> Unit // showVideoChatPrototype: () -> Unit ) { val uriHandler = LocalUriHandler.current @@ -114,7 +101,7 @@ fun SettingsLayout( Column( Modifier .fillMaxSize() - .background(if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) + .background(if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) .padding(top = 16.dp) ) { Text( @@ -142,7 +129,7 @@ fun SettingsLayout( SectionDivider() SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped) SectionDivider() - SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showAppearance, disabled = stopped) + SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(showCustomModal) }, disabled = stopped) SectionDivider() SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped) } @@ -288,7 +275,7 @@ fun SettingsLayout( tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal)) + Text(generalGetString(R.string.install_simplex_chat_for_terminal), color = MaterialTheme.colors.primary) } } @@ -355,7 +342,6 @@ fun PreviewSettingsLayout() { showSettingsModal = { {} }, showCustomModal = { {} }, showTerminal = {}, - showAppearance = {}, // showVideoChatPrototype = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt new file mode 100644 index 0000000000..9f410a1bc9 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt @@ -0,0 +1,69 @@ +package chat.simplex.app.views.usersettings + +import SectionItemViewSpaceBetween +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.ui.theme.* + +@Composable +fun ThemeSelectorView() { + val darkTheme = isSystemInDarkTheme() + val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme)) } + + ThemeSelectorLayout( + allThemes, + onSelectTheme = { + ThemeManager.applyTheme(it, darkTheme) + }, + ) +} + +@Composable fun ThemeSelectorLayout( + allThemes: List>, + onSelectTheme: (String) -> Unit, +) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Text( + stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current), + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + val currentTheme by CurrentColors.collectAsState() + SectionView(null) { + LazyColumn( + Modifier.padding(horizontal = 8.dp) + ) { + items(allThemes.size) { index -> + val item = allThemes[index] + val onClick = { + onSelectTheme(item.second.name) + } + SectionItemViewSpaceBetween(onClick, padding = PaddingValues()) { + Text(item.third) + if (currentTheme.second == item.second) { + Icon(Icons.Outlined.Check, item.third, tint = HighOrLowlight) + } + } + Spacer(Modifier.padding(horizontal = 4.dp)) + } + } + } + } +} + diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index b8246adb74..994672296a 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -272,7 +272,7 @@ Блокировка SimpleX Консоль SMP серверы - SimpleX Chat для терминала + SimpleX Chat для терминала Использовать серверы предосталенные SimpleX Chat? Сохраненные SMP серверы будут удалены. Ваши SMP серверы @@ -468,6 +468,7 @@ Экспериментальные функции SOCKS ПРОКСИ ИКОНКА + ТЕМЫ Данные чата @@ -635,4 +636,15 @@ Обновить настройки сети? Обновление настроек приведет к переподключению клиента ко всем серверам. Обновить + + + Системная + Светлая + Темная + + + Тема + Сохранить цвет + Сбросить цвета + Акцент diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index ca4aeb42e9..8019e862ab 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -277,7 +277,7 @@ SimpleX Lock Chat console SMP servers - Install SimpleX Chat for terminal + Install SimpleX Chat for terminal Use SimpleX Chat servers? Saved SMP servers will be removed. Your SMP servers @@ -470,6 +470,7 @@ Experimental features SOCKS PROXY APP ICON + THEMES Your chat database @@ -637,4 +638,15 @@ Update network settings? Updating settings will re-connect the client to all servers. Update + + + System + Light + Dark + + + Theme + Save color + Reset colors + Accent From 3a077d927d1a108de2a351419529793e4e64ecc4 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 25 Aug 2022 17:36:26 +0400 Subject: [PATCH 54/89] ios: contact aliases (#970) * ios: contact aliases * wip * wip * wip * move onTapGesture * revert test * improve search * corrections * font size * remove parameter * clear button * button style * remove clear button * ternary * refactor search * rename * ios: contact aliases translations (#972) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 10 +++- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 48 +++++++++++++++-- apps/ios/Shared/Views/Chat/ChatView.swift | 4 +- .../Views/Chat/Group/GroupChatInfoView.swift | 6 +-- .../Chat/Group/GroupMemberInfoView.swift | 4 +- .../Shared/Views/ChatList/ChatListView.swift | 13 +++-- .../Views/UserSettings/UserProfile.swift | 2 +- .../en.xcloc/Localized Contents/en.xliff | 5 ++ .../ru.xcloc/Localized Contents/ru.xliff | 7 ++- apps/ios/SimpleXChat/APITypes.swift | 10 +++- apps/ios/SimpleXChat/ChatTypes.swift | 53 +++++++++++++++---- apps/ios/ru.lproj/Localizable.strings | 5 +- 12 files changed, 137 insertions(+), 30 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6651b577b2..ea5df407da 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -324,9 +324,9 @@ func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile throw r } -func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?, Profile?) { +func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?, LocalProfile?) { let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .groupMemberInfo(_, _, connStats_, mainProfile) = r { return (connStats_, mainProfile) } + if case let .groupMemberInfo(_, _, connStats_, localMainProfile) = r { return (connStats_, localMainProfile) } throw r } @@ -435,6 +435,12 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? { } } +func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? { + let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias)) + if case let .contactAliasUpdated(toContact) = r { return toContact } + throw r +} + func apiCreateUserAddress() async throws -> String { let r = await chatSendCmd(.createMyAddress) if case let .userContactLinkCreated(connReq) = r { return connReq } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 0450f79816..62449f90a0 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -46,8 +46,11 @@ struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat + var contact: Contact var connectionStats: ConnectionStats? var customUserProfile: Profile? + @State var localAlias: String + @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -64,6 +67,14 @@ struct ChatInfoView: View { List { contactInfoHeader() .listRowBackground(Color.clear) + .contentShape(Rectangle()) + .onTapGesture { + aliasTextFieldFocused = false + } + + localAliasTextEdit() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) if let customUserProfile = customUserProfile { Section("Incognito") { @@ -113,11 +124,11 @@ struct ChatInfoView: View { .frame(width: 192, height: 192) .padding(.top, 12) .padding() - Text(cInfo.displayName) + Text(contact.profile.displayName) .font(.largeTitle) .lineLimit(1) .padding(.bottom, 2) - if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName { + if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName { Text(cInfo.fullName) .font(.title2) .lineLimit(2) @@ -126,6 +137,37 @@ struct ChatInfoView: View { .frame(maxWidth: .infinity, alignment: .center) } + func localAliasTextEdit() -> some View { + TextField("Set contact name…", text: $localAlias) + .disableAutocorrection(true) + .focused($aliasTextFieldFocused) + .submitLabel(.done) + .onChange(of: aliasTextFieldFocused) { focused in + if !focused { + setContactAlias() + } + } + .onSubmit { + setContactAlias() + } + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + } + + private func setContactAlias() { + Task { + do { + if let contact = try await apiSetContactAlias(contactId: chat.chatInfo.apiId, localAlias: localAlias) { + await MainActor.run { + chatModel.updateContact(contact) + } + } + } catch { + logger.error("setContactAlias error: \(responseError(error))") + } + } + } + func networkStatusRow() -> some View { HStack { Text("Network status") @@ -209,6 +251,6 @@ struct ChatInfoView: View { struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { - ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) + ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "") } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 049637f2d1..b5ea8afcc4 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -101,8 +101,8 @@ struct ChatView: View { } .sheet(isPresented: $showChatInfoSheet) { switch cInfo { - case .direct: - ChatInfoView(chat: chat, connectionStats: connectionStats, customUserProfile: customUserProfile) + case let .direct(contact): + ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias) case let .group(groupInfo): GroupChatInfoView(chat: chat, groupInfo: groupInfo) default: diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 68f402c0eb..90e721d431 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -20,7 +20,7 @@ struct GroupChatInfoView: View { @State private var selectedMember: GroupMember? = nil @State private var showGroupProfile: Bool = false @State private var connectionStats: ConnectionStats? - @State private var mainProfile: Profile? + @State private var memberMainProfile: LocalProfile? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum GroupChatInfoViewAlert: Identifiable { @@ -53,7 +53,7 @@ struct GroupChatInfoView: View { let (stats, profile) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) await MainActor.run { connectionStats = stats - mainProfile = profile + memberMainProfile = profile } } catch let error { logger.error("apiGroupMemberInfo error: \(responseError(error))") @@ -67,7 +67,7 @@ struct GroupChatInfoView: View { AddGroupMembersView(chat: chat, groupInfo: groupInfo) } .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: mainProfile) + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: memberMainProfile) } .sheet(isPresented: $showGroupProfile) { GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 3c0b257b21..96127affc6 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -15,7 +15,7 @@ struct GroupMemberInfoView: View { var groupInfo: GroupInfo var member: GroupMember var connectionStats: ConnectionStats? - var mainProfile: Profile? + var mainProfile: LocalProfile? @State private var alert: GroupMemberInfoViewAlert? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -76,7 +76,7 @@ struct GroupMemberInfoView: View { } } - private func mainProfileRow(_ mainProfile: Profile) -> some View { + private func mainProfileRow(_ mainProfile: LocalProfile) -> some View { HStack { Text("Known main profile") Spacer() diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 71bdd682c4..4a4dd4d658 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -77,9 +77,16 @@ struct ChatListView: View { let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase return s == "" ? chatModel.chats - : chatModel.chats.filter { - $0.chatInfo.chatType != .contactConnection && - $0.chatInfo.chatViewName.localizedLowercase.contains(s) + : chatModel.chats.filter { chat in + let contains = chat.chatInfo.chatViewName.localizedLowercase.contains(s) + switch chat.chatInfo { + case let .direct(contact): + return contains + || contact.profile.displayName.localizedLowercase.contains(s) + || contact.fullName.localizedLowercase.contains(s) + case .contactConnection: return false + default: return contains + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index a008b3d912..05951e5d31 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -142,7 +142,7 @@ struct UserProfile: View { if let newProfile = try await apiUpdateProfile(profile: profile) { DispatchQueue.main.async { if let profileId = chatModel.currentUser?.profile.profileId { - chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile) + chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile, "") } profile = newProfile } diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 32ee0be336..54abdd9d69 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1648,6 +1648,11 @@ We will be adding server redundancy to prevent lost messages. Servers No comment provided by engineer. + + Set contact name… + Set contact name… + No comment provided by engineer. + Set timeouts for proxy/VPN Set timeouts for proxy/VPN diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 50ab0d6b07..7902c5b90c 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -194,7 +194,7 @@ Accent color - Цвет акцента + Основной цвет No comment provided by engineer. @@ -1648,6 +1648,11 @@ We will be adding server redundancy to prevent lost messages. Серверы No comment provided by engineer. + + Set contact name… + Имя контакта… + No comment provided by engineer. + Set timeouts for proxy/VPN Установить таймауты для прокси/VPN diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5fb8cb19ed..060c55ccd0 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -55,6 +55,7 @@ public enum ChatCommand { case apiClearChat(type: ChatType, id: Int64) case listContacts case apiUpdateProfile(profile: Profile) + case apiSetContactAlias(contactId: Int64, localAlias: String) case createMyAddress case deleteMyAddress case showMyAddress @@ -120,6 +121,7 @@ public enum ChatCommand { case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case .listContacts: return "/contacts" case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))" + case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" case .showMyAddress: return "/show_address" @@ -184,6 +186,7 @@ public enum ChatCommand { case .apiClearChat: return "apiClearChat" case .listContacts: return "listContacts" case .apiUpdateProfile: return "apiUpdateProfile" + case .apiSetContactAlias: return "apiSetContactAlias" case .createMyAddress: return "createMyAddress" case .deleteMyAddress: return "deleteMyAddress" case .showMyAddress: return "showMyAddress" @@ -229,7 +232,7 @@ public enum ChatResponse: Decodable, Error { case userSMPServers(smpServers: [String]) case networkConfig(networkConfig: NetCfg) case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) - case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?, mainProfile: Profile?) + case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?, localMainProfile: LocalProfile?) case invitation(connReqInvitation: String) case sentConfirmation case sentInvitation @@ -238,6 +241,7 @@ public enum ChatResponse: Decodable, Error { case chatCleared(chatInfo: ChatInfo) case userProfileNoChange case userProfileUpdated(fromProfile: Profile, toProfile: Profile) + case contactAliasUpdated(toContact: Contact) case userContactLink(connReqContact: String) case userContactLinkCreated(connReqContact: String) case userContactLinkDeleted @@ -329,6 +333,7 @@ public enum ChatResponse: Decodable, Error { case .chatCleared: return "chatCleared" case .userProfileNoChange: return "userProfileNoChange" case .userProfileUpdated: return "userProfileUpdated" + case .contactAliasUpdated: return "contactAliasUpdated" case .userContactLink: return "userContactLink" case .userContactLinkCreated: return "userContactLinkCreated" case .userContactLinkDeleted: return "userContactLinkDeleted" @@ -411,7 +416,7 @@ public enum ChatResponse: Decodable, Error { case let .userSMPServers(smpServers): return String(describing: smpServers) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))" - case let .groupMemberInfo(groupInfo, member, connectionStats_, mainProfile): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))\nmainProfile: \(String(describing: mainProfile))" + case let .groupMemberInfo(groupInfo, member, connectionStats_, localMainProfile): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))\nlocalMainProfile: \(String(describing: localMainProfile))" case let .invitation(connReqInvitation): return connReqInvitation case .sentConfirmation: return noDetails case .sentInvitation: return noDetails @@ -420,6 +425,7 @@ public enum ChatResponse: Decodable, Error { case let .chatCleared(chatInfo): return String(describing: chatInfo) case .userProfileNoChange: return noDetails case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) + case let .contactAliasUpdated(toContact): return String(describing: toContact) case let .userContactLink(connReq): return connReq case let .userContactLinkCreated(connReq): return connReq case .userContactLinkDeleted: return noDetails diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index b5fea9e1bb..a15ac63df1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -19,6 +19,7 @@ public struct User: Decodable, NamedChat { public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } + public var localAlias: String { get { "" } } public static let sampleData = User( userId: 1, @@ -43,6 +44,7 @@ public struct Profile: Codable, NamedChat { public var displayName: String public var fullName: String public var image: String? + public var localAlias: String { get { "" } } var profileViewName: String { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" @@ -55,31 +57,36 @@ public struct Profile: Codable, NamedChat { } public struct LocalProfile: Codable, NamedChat { - public init(profileId: Int64, displayName: String, fullName: String, image: String? = nil) { + public init(profileId: Int64, displayName: String, fullName: String, image: String? = nil, localAlias: String) { self.profileId = profileId self.displayName = displayName self.fullName = fullName self.image = image + self.localAlias = localAlias } public var profileId: Int64 public var displayName: String public var fullName: String public var image: String? + public var localAlias: String var profileViewName: String { - (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" + localAlias == "" + ? (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" + : localAlias } static let sampleData = LocalProfile( profileId: 1, displayName: "alice", - fullName: "Alice" + fullName: "Alice", + localAlias: "" ) } -public func toLocalProfile (_ profileId: Int64, _ profile: Profile ) -> LocalProfile { - LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image) +public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile { + LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image, localAlias: localAlias) } public func fromLocalProfile (_ profile: LocalProfile) -> Profile { @@ -97,11 +104,14 @@ public protocol NamedChat { var displayName: String { get } var fullName: String { get } var image: String? { get } + var localAlias: String { get } } extension NamedChat { public var chatViewName: String { - get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") } + localAlias == "" + ? displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") + : localAlias } } @@ -157,6 +167,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var localAlias: String { + get { + switch self { + case let .direct(contact): return contact.localAlias + case let .group(groupInfo): return groupInfo.localAlias + case let .contactRequest(contactRequest): return contactRequest.localAlias + case let .contactConnection(contactConnection): return contactConnection.localAlias + } + } + } + public var id: ChatId { get { switch self { @@ -303,9 +324,10 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn.connStatus == .ready } } public var sendMsgEnabled: Bool { get { true } } - public var displayName: String { get { profile.displayName } } + public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } + public var localAlias: String { profile.localAlias } public var isIndirectContact: Bool { activeConn.connLevel > 0 || viaGroup != nil @@ -384,6 +406,7 @@ public struct UserContactRequest: Decodable, NamedChat { public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } + public var localAlias: String { "" } public static let sampleData = UserContactRequest( contactRequestId: 1, @@ -425,6 +448,7 @@ public struct PendingContactConnection: Decodable, NamedChat { } public var fullName: String { get { "" } } public var image: String? { get { nil } } + public var localAlias: String { "" } public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } public var incognito: Bool { @@ -524,6 +548,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { public var displayName: String { get { groupProfile.displayName } } public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } + public var localAlias: String { "" } public var canEdit: Bool { return membership.memberRole == .owner && membership.memberCurrent @@ -559,6 +584,7 @@ public struct GroupProfile: Codable, NamedChat { public var displayName: String public var fullName: String public var image: String? + public var localAlias: String { "" } public static let sampleData = GroupProfile( displayName: "team", @@ -581,7 +607,12 @@ public struct GroupMember: Identifiable, Decodable { public var activeConn: Connection? public var id: String { "#\(groupId) @\(groupMemberId)" } - public var displayName: String { get { memberProfile.displayName } } + public var displayName: String { + get { + let p = memberProfile + return p.localAlias == "" ? p.displayName : p.localAlias + } + } public var fullName: String { get { memberProfile.fullName } } public var image: String? { get { memberProfile.image } } @@ -598,7 +629,9 @@ public struct GroupMember: Identifiable, Decodable { public var chatViewName: String { get { let p = memberProfile - return p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + return p.localAlias == "" + ? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + : p.localAlias } } @@ -860,7 +893,7 @@ public struct ChatItem: Identifiable, Decodable { public var memberDisplayName: String? { get { if case let .groupRcv(groupMember) = chatDir { - return groupMember.memberProfile.displayName + return groupMember.displayName } else { return nil } diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 2e84149b57..23da2d5727 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -125,7 +125,7 @@ "above, then choose:" = "наверху, затем выберите:"; /* No comment provided by engineer. */ -"Accent color" = "Цвет акцента"; +"Accent color" = "Основной цвет"; /* accept contact request via notification accept incoming call via notification */ @@ -1175,6 +1175,9 @@ /* No comment provided by engineer. */ "Servers" = "Серверы"; +/* No comment provided by engineer. */ +"Set contact name…" = "Имя контакта…"; + /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; From 7683254de264cdb65a1480a9ddd7dddaef0de847 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:27:38 +0400 Subject: [PATCH 55/89] ios: group member navigation (#973) --- apps/ios/Shared/Views/Chat/ChatView.swift | 56 ++++++++++++++----- .../Views/Chat/Group/GroupChatInfoView.swift | 5 +- .../Chat/Group/GroupMemberInfoView.swift | 23 ++++++++ apps/ios/SimpleXChat/ChatTypes.swift | 2 +- 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index b5ea8afcc4..42dfb7b676 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -31,6 +31,10 @@ struct ChatView: View { @State private var searchMode = false @State private var searchText: String = "" @FocusState private var searchFocussed + // opening GroupMemberInfoView on member icon + @State private var selectedMember: GroupMember? = nil + @State private var memberConnectionStats: ConnectionStats? + @State private var memberMainProfile: LocalProfile? var body: some View { let cInfo = chat.chatInfo @@ -73,8 +77,8 @@ struct ChatView: View { } } ToolbarItem(placement: .principal) { - Button { - if case .direct = cInfo { + if case let .direct(contact) = cInfo { + Button { Task { do { let (stats, profile) = try await apiContactInfo(contactId: chat.chatInfo.apiId) @@ -87,7 +91,17 @@ struct ChatView: View { } await MainActor.run { showChatInfoSheet = true } } - } else if case let .group(groupInfo) = cInfo { + } label: { + ChatInfoToolbar(chat: chat) + } + .sheet(isPresented: $showChatInfoSheet, onDismiss: { + connectionStats = nil + customUserProfile = nil + }) { + ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias) + } + } else if case let .group(groupInfo) = cInfo { + Button { Task { let groupMembers = await apiListMembers(groupInfo.groupId) await MainActor.run { @@ -95,18 +109,11 @@ struct ChatView: View { showChatInfoSheet = true } } + } label: { + ChatInfoToolbar(chat: chat) } - } label: { - ChatInfoToolbar(chat: chat) - } - .sheet(isPresented: $showChatInfoSheet) { - switch cInfo { - case let .direct(contact): - ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias) - case let .group(groupInfo): + .sheet(isPresented: $showChatInfoSheet) { GroupChatInfoView(chat: chat, groupInfo: groupInfo) - default: - EmptyView() } } } @@ -342,13 +349,34 @@ struct ChatView: View { } @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { - if case let .groupRcv(member) = ci.chatDir { + if case let .groupRcv(member) = ci.chatDir, + case let .group(groupInfo) = chat.chatInfo { let prevItem = chatModel.getPrevChatItem(ci) HStack(alignment: .top, spacing: 0) { let showMember = prevItem == nil || showMemberImage(member, prevItem) if showMember { ProfileImage(imageStr: member.memberProfile.image) .frame(width: memberImageSize, height: memberImageSize) + .onTapGesture { + Task { + do { + let (stats, profile) = try await apiGroupMemberInfo(member.groupId, member.groupMemberId) + await MainActor.run { + memberConnectionStats = stats + memberMainProfile = profile + } + } catch let error { + logger.error("apiGroupMemberInfo error: \(responseError(error))") + } + await MainActor.run { selectedMember = member } + } + } + .sheet(item: $selectedMember, onDismiss: { + memberConnectionStats = nil + memberMainProfile = nil + }) { member in + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats, mainProfile: memberMainProfile) + } } else { Rectangle().fill(.clear) .frame(width: memberImageSize, height: memberImageSize) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 90e721d431..1ce709881e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -66,7 +66,10 @@ struct GroupChatInfoView: View { .sheet(isPresented: $showAddMembersSheet) { AddGroupMembersView(chat: chat, groupInfo: groupInfo) } - .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in + .sheet(item: $selectedMember, onDismiss: { + connectionStats = nil + memberMainProfile = nil + }) { member in GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: memberMainProfile) } .sheet(isPresented: $showGroupProfile) { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 96127affc6..5fa144bd75 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -31,6 +31,12 @@ struct GroupMemberInfoView: View { groupMemberInfoHeader() .listRowBackground(Color.clear) +// if let contactId = member.memberContactId { +// Section { +// openDirectChatButton(contactId) +// } +// } + Section("Member") { infoRow("Group", groupInfo.displayName) if let mainProfile = mainProfile { @@ -76,6 +82,23 @@ struct GroupMemberInfoView: View { } } + func openDirectChatButton(_ contactId: Int64) -> some View { + Button { + if let i = chatModel.chats.firstIndex(where: { chat in + switch chat.chatInfo { + case let .direct(contact): return contact.contactId == contactId + default: return false + } + }) { + dismissAllSheets(animated: true) + chatModel.chatId = chatModel.chats[i].chatInfo.id + } + } label: { + Label("Send direct message", systemImage: "message") + .foregroundColor(.accentColor) + } + } + private func mainProfileRow(_ mainProfile: LocalProfile) -> some View { HStack { Text("Known main profile") diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a15ac63df1..72664dabd3 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -311,7 +311,7 @@ public struct ChatStats: Decodable { } public struct Contact: Identifiable, Decodable, NamedChat { - var contactId: Int64 + public var contactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile public var activeConn: Connection From 2fc6873c42c18577f0267a83178e8dcdb5b92dff Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 27 Aug 2022 19:56:03 +0400 Subject: [PATCH 56/89] core: simplify incognito feature - remove host/invitee incognito profiles communication; remove incognito mode group creation and join; use same incognito profile known to host when joining (#978) --- docs/protocol/simplex-chat.schema.json | 12 - src/Simplex/Chat.hs | 119 +++--- src/Simplex/Chat/Controller.hs | 15 +- src/Simplex/Chat/Messages.hs | 15 +- .../M20220812_incognito_profiles.hs | 2 +- src/Simplex/Chat/Protocol.hs | 8 +- src/Simplex/Chat/Store.hs | 66 +--- src/Simplex/Chat/Types.hs | 1 - src/Simplex/Chat/View.hs | 126 +++--- tests/ChatTests.hs | 362 +++++++----------- tests/ProtocolTests.hs | 12 +- 11 files changed, 279 insertions(+), 459 deletions(-) diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index 76612d22fb..a0c6ec5706 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -108,12 +108,6 @@ "invitedMember": {"ref": "memberIdRole"}, "connRequest": {"ref": "connReqUri"}, "groupProfile": {"ref": "profile"} - }, - "optionalProperties": { - "fromMemberProfile": {"ref": "profile"}, - "metadata": { - "comment": "fromMemberProfile is user's custom profile to be used in the group - invitee should use this profile for the host's group member" - } } }, "memberIdRole": { @@ -323,12 +317,6 @@ "params": { "properties": { "memberId": {"ref": "base64url"} - }, - "optionalProperties": { - "memberProfile": {"ref": "profile"}, - "metadata": { - "comment": "memberProfile is user's custom profile to be used in the group - host should use this profile for the invitee's group member" - } } } } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 5fd93ec61b..de5e74fa62 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -625,12 +625,10 @@ processChatCommand = \case incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- withAgent (`getConnectionServers` contactConnId ct) pure $ CRContactInfo ct connectionStats (fmap fromLocalProfile incognitoProfile) - APIGroupMemberInfo gId gMemberId -> withUser $ \user@User {userId} -> do - -- [incognito] print group member main profile - (g, m@GroupMember {memberContactProfileId}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId - mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing + APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do + (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) - pure $ CRGroupMemberInfo g m connectionStats mainProfile + pure $ CRGroupMemberInfo g m connectionStats ContactInfo cName -> withUser $ \User {userId} -> do contactId <- withStore $ \db -> getContactIdByName db userId cName processChatCommand $ APIContactInfo contactId @@ -724,32 +722,29 @@ processChatCommand = \case processChatCommand $ APIUpdateChatItem chatRef editedItemId mc NewGroup gProfile -> withUser $ \user -> do gVar <- asks idsDrg - -- [incognito] create membership with incognito profile - incognito <- readTVarIO =<< asks incognitoMode - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile incognitoProfile) - pure $ CRGroupCreated groupInfo incognitoProfile + groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile) + pure $ CRGroupCreated groupInfo APIAddMember groupId contactId memRole -> withUser $ \user@User {userId} -> withChatLock $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db userId contactId - -- [incognito] forbid to invite contact to whom user is connected as incognito if user's membership is not incognito let Group gInfo@GroupInfo {localDisplayName, groupProfile, membership} members = group GroupMember {memberRole = userRole, memberId = userMemberId} = membership Contact {localDisplayName = cName} = contact - when (contactConnIncognito contact && not (memberIncognito membership)) $ throwChatError CEGroupNotIncognitoCantInvite + -- [incognito] forbid to invite contact to whom user is connected incognito + when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite + -- [incognito] forbid to invite contacts if user joined the group using an incognito profile + when (memberIncognito membership) $ throwChatError CEGroupIncognitoCantInvite when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo) unless (memberActive membership) $ throwChatError CEGroupMemberNotActive let sendInvitation member@GroupMember {groupMemberId, memberId} cReq = do - -- [incognito] if membership is incognito, send its incognito profile in GroupInvitation - let incognitoProfile = if memberIncognito membership then Just (fromLocalProfile $ memberProfile membership) else Nothing - groupInv = GroupInvitation (MemberIdRole userMemberId userRole) incognitoProfile (MemberIdRole memberId memRole) cReq groupProfile + let groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile msg <- sendDirectContactMessage contact $ XGrpInv groupInv - let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just $ memberIncognito membership}) memRole + let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveSndChatItem user (CDDirectSnd contact) msg content Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat contact) ci setActive $ ActiveG localDisplayName - pure $ CRSentGroupInvitation gInfo contact member incognitoProfile + pure $ CRSentGroupInvitation gInfo contact member case contactMember contact members of Nothing -> do gVar <- asks idsDrg @@ -765,24 +760,13 @@ processChatCommand = \case APIJoinGroup groupId -> withUser $ \user@User {userId} -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId withChatLock . procCmd $ do - -- [incognito] if incognito mode is enabled [AND membership is not incognito] update membership to use incognito profile - incognito <- readTVarIO =<< asks incognitoMode - g'@GroupInfo {membership = membership'} <- - if incognito && not (memberIncognito membership) - then do - incognitoProfile <- liftIO generateRandomProfile - membership' <- withStore $ \db -> createMemberIncognitoProfile db userId membership (Just incognitoProfile) - pure g {membership = membership'} - else pure g - -- [incognito] if membership is incognito, send its incognito profile in XGrpAcpt - let incognitoProfile = if memberIncognito membership' then Just (fromLocalProfile $ memberProfile membership') else Nothing - agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile + agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId updateGroupMemberStatus db userId fromMember GSMemAccepted - updateGroupMemberStatus db userId membership' GSMemAccepted + updateGroupMemberStatus db userId membership GSMemAccepted updateCIGroupInvitationStatus user - pure $ CRUserAcceptedGroupSent g' {membership = membership' {memberStatus = GSMemAccepted}} + pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}} where updateCIGroupInvitationStatus user@User {userId} = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db user groupId @@ -1445,10 +1429,11 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci _ -> pure () - Just (gInfo, m@GroupMember {activeConn}) -> do + Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> do when (maybe False ((== ConnReady) . connStatus) activeConn) $ do notifyMemberConnected gInfo m - when (memberCategory m == GCPreMember) $ probeMatchingContacts ct + let connectedIncognito = contactConnIncognito ct || memberIncognito membership + when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito SENT msgId -> do sentMsgDeliveryEvent conn msgId withStore' (\db -> getDirectChatItemByAgentMsgId db userId contactId connId msgId) >>= \case @@ -1471,18 +1456,15 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> pure () processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m () - processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m@GroupMember {memberContactProfileId} = case agentMsg of + processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m = case agentMsg of CONF confId _ connInfo -> do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo case memberCategory m of GCInviteeMember -> case chatMsgEvent of - XGrpAcpt memId incognitoProfile + XGrpAcpt memId | sameMemberId memId m -> do - -- [incognito] update member profile to incognito profile - withStore $ \db -> do - liftIO $ updateGroupMemberStatus db userId m GSMemAccepted - void $ createMemberIncognitoProfile db userId m incognitoProfile + withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted allowAgentConnection conn confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" _ -> messageError "CONF from invited member must have x.grp.acpt" @@ -1491,7 +1473,6 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage XGrpMemInfo memId _memProfile | sameMemberId memId m -> do -- TODO update member profile - -- [incognito] send membership incognito profile allowAgentConnection conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" _ -> messageError "CONF from member must have x.grp.mem.info" @@ -1516,17 +1497,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unless (enableNtfs chatSettings) . withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) False case memberCategory m of GCHostMember -> do - -- [incognito] chat item & event with indication that host connected incognito - mainProfile <- fmap fromLocalProfile <$> if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing - memberConnectedChatItem gInfo m mainProfile - toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} (memberIncognito membership) + memberConnectedChatItem gInfo m + toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} setActive $ ActiveG gName showToast ("#" <> gName) "you are connected to group" GCInviteeMember -> do - -- [incognito] chat item & event with indication that invitee connected incognito - mainProfile <- fmap fromLocalProfile <$> if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing - memberConnectedChatItem gInfo m mainProfile - toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} mainProfile + memberConnectedChatItem gInfo m + toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} setActive $ ActiveG gName showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected" intros <- withStore' $ \db -> createIntroductions db members m @@ -1544,7 +1521,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage Just ct@Contact {activeConn = Connection {connStatus}} -> when (connStatus == ConnReady) $ do notifyMemberConnected gInfo m - when (memberCategory m == GCPreMember) $ probeMatchingContacts ct + let connectedIncognito = contactConnIncognito ct || memberIncognito membership + when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito MSG msgMeta _msgFlags msgBody -> do msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody withAckMessage agentConnId msgMeta $ @@ -1713,12 +1691,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage cancelRcvFileTransfer user ft throwChatError $ CEFileRcvChunk err - memberConnectedChatItem :: GroupInfo -> GroupMember -> Maybe Profile -> m () - memberConnectedChatItem gInfo m mainProfile_ = do + memberConnectedChatItem :: GroupInfo -> GroupMember -> m () + memberConnectedChatItem gInfo m = do createdAt <- liftIO getCurrentTime - let content = CIRcvGroupEvent $ case mainProfile_ of - Just mainProfile -> RGEMemberConnected $ Just mainProfile - _ -> RGEMemberConnected Nothing + let content = CIRcvGroupEvent RGEMemberConnected cd = CDGroupRcv gInfo m -- first ts should be broker ts but we don't have it for CON ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content createdAt createdAt @@ -1727,20 +1703,24 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage notifyMemberConnected :: GroupInfo -> GroupMember -> m () notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do - memberConnectedChatItem gInfo m Nothing + memberConnectedChatItem gInfo m toView $ CRConnectedToGroupMember gInfo m let g = groupName' gInfo setActive $ ActiveG g showToast ("#" <> g) $ "member " <> c <> " is connected" - probeMatchingContacts :: Contact -> m () - probeMatchingContacts ct = do + probeMatchingContacts :: Contact -> Bool -> m () + probeMatchingContacts ct connectedIncognito = do gVar <- asks idsDrg (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct void . sendDirectContactMessage ct $ XInfoProbe probe - cs <- withStore' $ \db -> getMatchingContacts db userId ct - let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) - forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ()) + if connectedIncognito + then + withStore' $ \db -> deleteSentProbe db userId probeId + else do + cs <- withStore' $ \db -> getMatchingContacts db userId ct + let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) + forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ()) where sendProbeHash :: Contact -> ProbeHash -> Int64 -> m () sendProbeHash c probeHash probeId = do @@ -1915,20 +1895,17 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () - processGroupInvitation ct@Contact {contactId, localDisplayName = c} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), fromMemberProfile, invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do + processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId - -- [incognito] if (received group invitation has host's incognito profile OR direct connection with host is incognito), create membership with new incognito profile; incognito mode is checked when joining group - hostContact <- withStore $ \db -> getContact db userId contactId - let joinGroupIncognito = isJust fromMemberProfile || contactConnIncognito hostContact - incognitoProfile <- if joinGroupIncognito then Just <$> liftIO generateRandomProfile else pure Nothing - gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv incognitoProfile - let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just joinGroupIncognito}) memRole + -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile + gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId + let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - toView $ CRReceivedGroupInvitation gInfo ct memRole fromMemberProfile + toView $ CRReceivedGroupInvitation gInfo ct memRole showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group" checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m () @@ -2111,8 +2088,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage else do (groupConnId, groupConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation (directConnId, directConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation - -- [incognito] direct connection with member has to be established using same incognito profile - customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing + -- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership] + let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId customUserProfileId let msg = XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq} void $ sendDirectMessage conn msg (GroupId groupId) @@ -2146,7 +2123,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) groupConnId <- withAgent $ \a -> joinConnection a True groupConnReq $ directMessage msg directConnId <- withAgent $ \a -> joinConnection a True directConnReq $ directMessage msg - customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing + let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId customUserProfileId xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> MsgMeta -> m () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e1088a050b..0096ae7337 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -215,7 +215,7 @@ data ChatResponse | CRUserSMPServers {smpServers :: [SMPServer]} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} - | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats, localMainProfile :: Maybe LocalProfile} + | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} | CRNewChatItem {chatItem :: AChatItem} | CRChatItemStatusUpdated {chatItem :: AChatItem} | CRChatItemUpdated {chatItem :: AChatItem} @@ -227,7 +227,7 @@ data ChatResponse | CRCmdOk | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} - | CRGroupCreated {groupInfo :: GroupInfo, customUserProfile :: Maybe Profile} + | CRGroupCreated {groupInfo :: GroupInfo} | CRGroupMembers {group :: Group} | CRContactsList {contacts :: [Contact]} | CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent} @@ -236,7 +236,7 @@ data ChatResponse | CRUserAcceptedGroupSent {groupInfo :: GroupInfo} | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} | CRGroupsList {groups :: [GroupInfo]} - | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember, sentCustomProfile :: Maybe Profile} + | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus | CRUserProfile {profile :: Profile} | CRUserProfileNoChange @@ -279,9 +279,9 @@ data ChatResponse | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRGroupInvitation {groupInfo :: GroupInfo} - | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole, receivedCustomProfile :: Maybe Profile} - | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember, usedCustomProfile :: Bool} - | CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember, mainProfile :: Maybe Profile} + | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} + | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember} + | CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember} | CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} | CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember} | CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} @@ -388,7 +388,8 @@ data ChatErrorType | CEContactNotReady {contact :: Contact} | CEContactGroups {contact :: Contact, groupNames :: [GroupName]} | CEGroupUserRole - | CEGroupNotIncognitoCantInvite + | CEContactIncognitoCantInvite + | CEGroupIncognitoCantInvite | CEGroupContactRole {contactName :: ContactName} | CEGroupDuplicateMember {contactName :: ContactName} | CEGroupDuplicateMemberId diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 8633d25207..07f77478fb 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -502,9 +502,7 @@ ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayN rcvGroupEventToText :: RcvGroupEvent -> Text rcvGroupEventToText = \case RGEMemberAdded _ p -> "added " <> profileToText p - RGEMemberConnected contactMainProfile -> case contactMainProfile of - Just p -> profileToText p <> " connected incognito" - Nothing -> "connected" + RGEMemberConnected -> "connected" RGEMemberLeft -> "left" RGEMemberDeleted _ p -> "removed " <> profileToText p RGEUserDeleted -> "removed you" @@ -535,15 +533,15 @@ data CIContent (d :: MsgDirection) where CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd --- ^^^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API --- ! ^^^ Nested sum types also have to use different encodings for database and API --- ! ^^^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent +-- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API +-- ! ^ Nested sum types also have to use different encodings for database and API +-- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent deriving instance Show (CIContent d) data RcvGroupEvent = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting - | RGEMemberConnected {contactMainProfile :: Maybe Profile} -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember + | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember | RGEMemberLeft -- CRLeftMember | RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember | RGEUserDeleted -- CRDeletedMemberUser @@ -594,8 +592,7 @@ data CIGroupInvitation = CIGroupInvitation groupMemberId :: GroupMemberId, localDisplayName :: GroupName, groupProfile :: GroupProfile, - status :: CIGroupInvitationStatus, - invitedIncognito :: Maybe Bool + status :: CIGroupInvitationStatus } deriving (Eq, Show, Generic, FromJSON) diff --git a/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs b/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs index 2ece1be90d..e03eda2358 100644 --- a/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs +++ b/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs @@ -10,7 +10,7 @@ m20220812_incognito_profiles = [sql| ALTER TABLE connections ADD COLUMN custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- only set for direct connections -ALTER TABLE group_members ADD COLUMN member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- member profile id if incognito profile was saved for member (used for hosts and invitees in incognito groups) +ALTER TABLE group_members ADD COLUMN member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- member profile id if incognito profile was saved for member (used when invitation is received via incognito direct connection with host) ALTER TABLE contact_profiles ADD COLUMN incognito INTEGER; -- 1 for incognito |] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 3a957fbe2b..8b6a402288 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -124,7 +124,7 @@ data ChatMsgEvent | XInfo Profile | XContact Profile (Maybe XContactId) | XGrpInv GroupInvitation - | XGrpAcpt MemberId (Maybe Profile) + | XGrpAcpt MemberId | XGrpMemNew MemberInfo | XGrpMemIntro MemberInfo | XGrpMemInv MemberId IntroInvitation @@ -413,7 +413,7 @@ toCMEventTag = \case XInfo _ -> XInfo_ XContact _ _ -> XContact_ XGrpInv _ -> XGrpInv_ - XGrpAcpt _ _ -> XGrpAcpt_ + XGrpAcpt _ -> XGrpAcpt_ XGrpMemNew _ -> XGrpMemNew_ XGrpMemIntro _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -479,7 +479,7 @@ appToChatMessage AppMessage {msgId, event, params} = do XInfo_ -> XInfo <$> p "profile" XContact_ -> XContact <$> p "profile" <*> opt "contactReqId" XGrpInv_ -> XGrpInv <$> p "groupInvitation" - XGrpAcpt_ -> XGrpAcpt <$> p "memberId" <*> opt "memberProfile" + XGrpAcpt_ -> XGrpAcpt <$> p "memberId" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -521,7 +521,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p XInfo profile -> o ["profile" .= profile] XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile] XGrpInv groupInv -> o ["groupInvitation" .= groupInv] - XGrpAcpt memId profile -> o $ ("memberProfile" .=? profile) ["memberId" .= memId] + XGrpAcpt memId -> o ["memberId" .= memId] XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] XGrpMemIntro memInfo -> o ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 198e2e2d0a..e4391d651c 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -82,14 +82,12 @@ module Simplex.Chat.Store getMemberInvitation, createMemberConnection, updateGroupMemberStatus, - createMemberIncognitoProfile, createNewGroupMember, deleteGroupMember, deleteGroupMemberConnection, createIntroductions, updateIntroStatus, saveIntroInvitation, - getGroupMemberProfileId, createIntroReMember, createIntroToMemberContact, saveMemberInvitation, @@ -99,6 +97,7 @@ module Simplex.Chat.Store randomBytes, createSentProbe, createSentProbeHash, + deleteSentProbe, matchReceivedProbe, matchReceivedProbeHash, matchSentProbe, @@ -1046,6 +1045,13 @@ createSentProbeHash db userId probeId _to@Contact {contactId} = do "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" (probeId, contactId, userId, currentTs, currentTs) +deleteSentProbe :: DB.Connection -> UserId -> Int64 -> IO () +deleteSentProbe db userId probeId = + DB.execute + db + "DELETE FROM sent_probes WHERE user_id = ? AND sent_probe_id = ?" + (userId, probeId) + matchReceivedProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact) matchReceivedProbe db userId _from@Contact {contactId} (Probe probe) = do let probeHash = C.sha256Hash probe @@ -1325,8 +1331,8 @@ updateConnectionStatus db Connection {connId} connStatus = do DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo -createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do +createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo +createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do let GroupProfile {displayName, fullName, image} = groupProfile currentTs <- getCurrentTime withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do @@ -1342,14 +1348,13 @@ createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = Except (ldn, userId, profileId, True, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 - -- TODO ldn from incognito profile - membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs + membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs let chatSettings = ChatSettings {enableNtfs = True} pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo -createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, fromMemberProfile, invitedMember, connRequest, groupProfile} incognitoProfile = do +createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO GroupInfo +createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ -- TODO treat the case that the invitation details could've changed @@ -1376,17 +1381,17 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" (profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs) insertedRowId db - _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs - membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs + _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs + membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs let chatSettings = ChatSettings {enableNtfs = True} pure GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs} -createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember -createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do - incognitoProfileId <- liftIO $ createIncognitoProfile_ db userId createdAt incognitoProfile +createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> ExceptT StoreError IO GroupMember +createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt = do + incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId (localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of - (Just profile@Profile {displayName}, Just profileId) -> - (,toLocalProfile profileId profile "") <$> insertMemberIncognitoProfile_ displayName profileId + (Just profile@LocalProfile {displayName}, Just profileId) -> + (,profile) <$> insertMemberIncognitoProfile_ displayName profileId _ -> (,profile' userOrContact) <$> liftIO insertMember_ groupMemberId <- liftIO $ insertedRowId db pure @@ -1660,25 +1665,6 @@ updateGroupMemberStatus db userId GroupMember {groupMemberId} memStatus = do |] (memStatus, currentTs, userId, groupMemberId) -createMemberIncognitoProfile :: DB.Connection -> UserId -> GroupMember -> Maybe Profile -> ExceptT StoreError IO GroupMember -createMemberIncognitoProfile db userId m@GroupMember {groupMemberId} incognitoProfile = do - currentTs <- liftIO getCurrentTime - incognitoProfileId <- liftIO $ createIncognitoProfile_ db userId currentTs incognitoProfile - case (incognitoProfile, incognitoProfileId) of - (Just profile@Profile {displayName}, Just profileId) -> - ExceptT $ - withLocalDisplayName db userId displayName $ \incognitoLdn -> do - DB.execute - db - [sql| - UPDATE group_members - SET local_display_name = ?, member_profile_id = ?, updated_at = ? - WHERE user_id = ? AND group_member_id = ? - |] - (incognitoLdn, profileId, currentTs, userId, groupMemberId) - pure . Right $ m {localDisplayName = incognitoLdn, memberProfile = toLocalProfile profileId profile ""} - _ -> pure m - -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image}) memCategory memStatus = @@ -1834,18 +1820,6 @@ getIntroduction_ db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -getGroupMemberProfileId :: DB.Connection -> UserId -> GroupMember -> ExceptT StoreError IO Int64 -getGroupMemberProfileId db userId GroupMember {groupMemberId, groupId} = - ExceptT . firstRow fromOnly (SEGroupMemberNotFound {groupId, groupMemberId}) $ - DB.query - db - [sql| - SELECT contact_profile_id - FROM group_members - WHERE user_id = ? AND group_member_id = ? - |] - (userId, groupMemberId) - createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> Maybe ProfileId -> ExceptT StoreError IO GroupMember createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId customUserProfileId = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 93612bd6a6..51abca2a7a 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -282,7 +282,6 @@ instance FromField ImageData where fromField = fmap ImageData . fromField data GroupInvitation = GroupInvitation { fromMember :: MemberIdRole, - fromMemberProfile :: Maybe Profile, invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 31b159af18..8e9825abb3 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -65,7 +65,7 @@ responseToView testView = \case CRUserSMPServers smpServers -> viewSMPServers smpServers testView CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile - CRGroupMemberInfo g m cStats mainProfile -> viewGroupMemberInfo g m cStats mainProfile + CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item False CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems CRChatItemStatusUpdated _ -> [] @@ -89,10 +89,10 @@ responseToView testView = \case CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] - CRGroupCreated g customUserProfile -> viewGroupCreated g customUserProfile testView + CRGroupCreated g -> viewGroupCreated g CRGroupMembers g -> viewGroupMembers g CRGroupsList gs -> viewGroupsList gs - CRSentGroupInvitation g c _ sentCustomProfile -> viewSentGroupInvitation g c sentCustomProfile + CRSentGroupInvitation g c _ -> viewSentGroupInvitation g c CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus CRUserProfile p -> viewUserProfile p CRUserProfileNoChange -> ["user profile did not change"] @@ -139,11 +139,10 @@ responseToView testView = \case [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" where (errors, subscribed) = partition (isJust . contactError) summary - CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} -> - [groupInvitation' ldn fullName $ memberIncognito membership] - CRReceivedGroupInvitation g c role receivedCustomProfile -> viewReceivedGroupInvitation g c role receivedCustomProfile - CRUserJoinedGroup g _ usedCustomProfile -> viewUserJoinedGroup g usedCustomProfile testView - CRJoinedGroupMember g m mainProfile -> viewJoinedGroupMember g m mainProfile + CRGroupInvitation g -> [groupInvitation' g] + CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role + CRUserJoinedGroup g _ -> viewUserJoinedGroup g + CRJoinedGroupMember g m -> viewJoinedGroupMember g m CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] CRJoinedGroupMemberConnecting g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] @@ -370,11 +369,9 @@ viewConnReqInvitation cReq = "and ask them to connect: " <> highlight' "/c " ] -viewSentGroupInvitation :: GroupInfo -> Contact -> Maybe Profile -> [StyledString] -viewSentGroupInvitation g c sentCustomProfile = - if isJust sentCustomProfile - then ["invitation to join the group " <> ttyGroup' g <> " incognito sent to " <> ttyContact' c] - else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] +viewSentGroupInvitation :: GroupInfo -> Contact -> [StyledString] +viewSentGroupInvitation g c = + ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of @@ -428,22 +425,11 @@ viewReceivedContactRequest c Profile {fullName} = "to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)" ] -viewGroupCreated :: GroupInfo -> Maybe Profile -> Bool -> [StyledString] -viewGroupCreated g@GroupInfo {localDisplayName} incognitoProfile testView = - case incognitoProfile of - Just profile -> - if testView - then incognitoProfile' profile : message - else message - where - message = - [ "group " <> ttyFullGroup g <> " is created incognito, your profile for this group: " <> incognitoProfile' profile, - "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" - ] - Nothing -> - [ "group " <> ttyFullGroup g <> " is created", - "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" - ] +viewGroupCreated :: GroupInfo -> [StyledString] +viewGroupCreated g@GroupInfo {localDisplayName} = + [ "group " <> ttyFullGroup g <> " is created", + "use " <> highlight ("/a " <> localDisplayName <> " ") <> " to add members" + ] viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = @@ -451,33 +437,22 @@ viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = "to re-send invitation: " <> highlight ("/rm " <> gn <> " " <> c) <> ", " <> highlight ("/a " <> gn <> " " <> c) ] -viewUserJoinedGroup :: GroupInfo -> Bool -> Bool -> [StyledString] -viewUserJoinedGroup g@GroupInfo {membership = GroupMember {memberProfile}} incognito testView = - if incognito - then - if testView - then incognitoProfile' (fromLocalProfile memberProfile) : incognitoMessage - else incognitoMessage +viewUserJoinedGroup :: GroupInfo -> [StyledString] +viewUserJoinedGroup g@GroupInfo {membership = membership@GroupMember {memberProfile}} = + if memberIncognito membership + then [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] else [ttyGroup' g <> ": you joined the group"] - where - incognitoMessage = [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] -viewJoinedGroupMember :: GroupInfo -> GroupMember -> Maybe Profile -> [StyledString] -viewJoinedGroupMember g m@GroupMember {localDisplayName} = \case - Just Profile {displayName = mainProfileName} -> [ttyGroup' g <> ": " <> ttyContact mainProfileName <> " joined the group incognito as " <> styleIncognito localDisplayName] - Nothing -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] +viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString] +viewJoinedGroupMember g m = + [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] -viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> Maybe Profile -> [StyledString] -viewReceivedGroupInvitation g c role hostIncognitoProfile = - case hostIncognitoProfile of - Just profile -> - [ ttyFullGroup g <> ": " <> ttyContact' c <> " (known to the group as " <> incognitoProfile' profile <> ") invites you to join the group incognito as " <> plain (strEncode role), - "use " <> highlight ("/j " <> groupName' g) <> " to join this group incognito" - ] - Nothing -> - [ ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role), - "use " <> highlight ("/j " <> groupName' g) <> " to accept" - ] +viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] +viewReceivedGroupInvitation g@GroupInfo {membership = membership@GroupMember {memberProfile}} c role = + ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) : + if memberIncognito membership + then ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] + else ["use " <> highlight ("/j " <> groupName' g) <> " to accept"] groupPreserved :: GroupInfo -> [StyledString] groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"] @@ -529,9 +504,9 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g GroupName) - groupSS GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} = + groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} = case memberStatus membership of - GSMemInvited -> groupInvitation' ldn fullName $ memberIncognito membership + GSMemInvited -> groupInvitation' g s -> incognito <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s where incognito = if memberIncognito membership then incognitoPrefix else "" @@ -542,20 +517,20 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs _ -> "" delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" -groupInvitation' :: GroupName -> Text -> Bool -> StyledString -groupInvitation' displayName fullName membershipIncognito = - highlight ("#" <> displayName) - <> optFullName displayName fullName - <> invitationText - <> highlight ("/j " <> displayName) - <> " to join, " - <> highlight ("/d #" <> displayName) +groupInvitation' :: GroupInfo -> StyledString +groupInvitation' GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership = membership@GroupMember {memberProfile}} = + highlight ("#" <> ldn) + <> optFullName ldn fullName + <> " - you are invited (" + <> highlight ("/j " <> ldn) + <> joinText + <> highlight ("/d #" <> ldn) <> " to delete invitation)" where - invitationText = - if membershipIncognito - then " - you are invited incognito (" - else " - you are invited (" + joinText = + if memberIncognito membership + then " to join as " <> incognitoProfile' (fromLocalProfile memberProfile) <> ", " + else " to join, " viewContactsMerged :: Contact -> Contact -> [StyledString] viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} = @@ -603,21 +578,13 @@ viewContactInfo Contact {contactId, profile = LocalProfile {localAlias}} stats i incognitoProfile <> if localAlias /= "" then ["alias: " <> plain localAlias] else ["alias not set"] -viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> Maybe LocalProfile -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias = mpLocalAlias}} stats mainProfile = +viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] +viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats - <> maybe - ["unknown whether group member uses his main profile or incognito one for the group"] - (\LocalProfile {displayName, fullName} -> ["member is using " <> styleIncognito' "incognito" <> " profile for the group, main profile known: " <> ttyFullName displayName fullName]) - mainProfile - <> if alias /= "" then ["alias: " <> plain alias] else ["no alias for contact"] - where - alias = case mainProfile of - Nothing -> mpLocalAlias - Just LocalProfile {localAlias = lpLocalAlias} -> lpLocalAlias + <> if localAlias /= "" then ["alias: " <> plain localAlias] else ["no alias for contact"] viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvServers, sndServers} = @@ -916,7 +883,8 @@ viewChatError = \case CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"] CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"] CEGroupUserRole -> ["you have insufficient permissions for this group command"] - CEGroupNotIncognitoCantInvite -> ["you're using main profile for this group - prohibited to invite contact to whom you are connected incognito"] + CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"] + CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> groupName' g)] CEGroupMemberNotActive -> ["you cannot invite other members yet, try later"] diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 06afb1595e..464cdd90cc 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -92,9 +92,8 @@ chatTests = do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress it "accept contact request incognito" testAcceptContactRequestIncognito - it "create group incognito" testCreateGroupIncognito it "join group incognito" testJoinGroupIncognito - it "can't invite contact to whom user connected incognito to non incognito group" testCantInviteIncognitoConnectionNonIncognitoGroup + it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito it "set contact alias" testSetAlias describe "SMP servers" $ it "get and set SMP servers" testGetSetSMPServers @@ -2147,278 +2146,199 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $ bob #> ("@" <> aliceIncognito <> " I know!") alice ?<# "bob> I know!" -testCreateGroupIncognito :: IO () -testCreateGroupIncognito = testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - -- non incognito connections - connectUsers alice cath - connectUsers bob cath - -- bob connected incognito to alice - alice ##> "/c" - inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - bobIncognito <- getTermLine bob - concurrentlyN_ - [ do - bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## "use /info alice to print out this incognito profile again", - alice <## (bobIncognito <> ": contact is connected") - ] - -- alice creates group incognito - alice #$> ("/incognito on", id, "ok") - alice ##> "/g secret_club" - aliceMemIncognito <- getTermLine alice - alice <## ("group #secret_club is created incognito, your profile for this group: " <> aliceMemIncognito) - alice <## "use /a secret_club to add members" - alice ##> ("/a secret_club " <> bobIncognito) - concurrentlyN_ - [ alice <## ("invitation to join the group #secret_club incognito sent to " <> bobIncognito), - do - bob <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin") - bob <## "use /j secret_club to join this group incognito" - ] - -- bob uses different profile when joining group - bob ##> "/j secret_club" - bobMemIncognito <- getTermLine bob - concurrently_ - (alice <## ("#secret_club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito)) - (bob <## ("#secret_club: you joined the group incognito as " <> bobMemIncognito)) - -- cath is invited incognito - alice ##> "/a secret_club cath" - concurrentlyN_ - [ alice <## "invitation to join the group #secret_club incognito sent to cath", - do - cath <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin") - cath <## "use /j secret_club to join this group incognito" - ] - cath ##> "/j secret_club" - cathMemIncognito <- getTermLine cath - -- bob and cath don't merge contacts - concurrentlyN_ - [ alice <## ("#secret_club: cath joined the group incognito as " <> cathMemIncognito), - do - cath <## ("#secret_club: you joined the group incognito as " <> cathMemIncognito) - cath <## ("#secret_club: member " <> bobMemIncognito <> " is connected"), - do - bob <## ("#secret_club: " <> aliceMemIncognito <> " added " <> cathMemIncognito <> " to the group (connecting...)") - bob <## ("#secret_club: new member " <> cathMemIncognito <> " is connected") - ] - -- send messages - group is incognito for everybody - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") - cath #$> ("/incognito off", id, "ok") - alice ?#> "#secret_club hello" - concurrently_ - (bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) - (cath ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) - bob ?#> "#secret_club hi there" - concurrently_ - (alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi there")) - (cath ?<# ("#secret_club " <> bobMemIncognito <> "> hi there")) - cath ?#> "#secret_club hey" - concurrently_ - (alice ?<# ("#secret_club " <> cathMemIncognito <> "> hey")) - (bob ?<# ("#secret_club " <> cathMemIncognito <> "> hey")) - -- bob and cath can send messages via direct incognito connections - bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob") - cath ?<# (bobMemIncognito <> "> hi, I'm bob") - cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath") - bob ?<# (cathMemIncognito <> "> hey, I'm cath") - -- non incognito connections are separate - bob <##> cath - -- list groups - alice ##> "/gs" - alice <## "i #secret_club" - -- list group members - alice ##> "/ms secret_club" - alice - <### [ "i " <> aliceMemIncognito <> ": owner, you, created group", - "i " <> bobMemIncognito <> ": admin, invited, connected", - "i " <> cathMemIncognito <> ": admin, invited, connected" - ] - -- remove member - bob ##> ("/rm secret_club " <> cathMemIncognito) - concurrentlyN_ - [ bob <## ("#secret_club: you removed " <> cathMemIncognito <> " from the group"), - alice <## ("#secret_club: " <> bobMemIncognito <> " removed " <> cathMemIncognito <> " from the group"), - do - cath <## ("#secret_club: " <> bobMemIncognito <> " removed you from the group") - cath <## "use /d #secret_club to delete the group" - ] - bob ?#> "#secret_club hi" - concurrently_ - (alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi")) - (cath "#secret_club hello" - concurrently_ - (bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello")) - (cath "#secret_club hello" - cath <## "you are no longer a member of the group" - bob ?#> ("@" <> cathMemIncognito <> " I removed you from group") - cath ?<# (bobMemIncognito <> "> I removed you from group") - cath ?#> ("@" <> bobMemIncognito <> " ok") - bob ?<# (cathMemIncognito <> "> ok") - testJoinGroupIncognito :: IO () testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do -- non incognito connections - connectUsers alice cath + connectUsers alice bob + connectUsers alice dan connectUsers bob cath + connectUsers bob dan connectUsers cath dan - -- bob connected incognito to alice + -- cath connected incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - bobIncognito <- getTermLine bob + cath #$> ("/incognito on", id, "ok") + cath ##> ("/c " <> inv) + cath <## "confirmation sent!" + cathIncognito <- getTermLine cath concurrentlyN_ [ do - bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## "use /info alice to print out this incognito profile again", - alice <## (bobIncognito <> ": contact is connected") + cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) + cath <## "use /info alice to print out this incognito profile again", + alice <## (cathIncognito <> ": contact is connected") ] - -- alice creates group non incognito - alice ##> "/g club" - alice <## "group #club is created" - alice <## "use /a club to add members" - alice ##> ("/a club " <> bobIncognito) + -- alice creates group + alice ##> "/g secret_club" + alice <## "group #secret_club is created" + alice <## "use /a secret_club to add members" + -- alice invites bob + alice ##> "/a secret_club bob" concurrentlyN_ - [ alice <## ("invitation to join the group #club sent to " <> bobIncognito), + [ alice <## "invitation to join the group #secret_club sent to bob", do - bob <## "#club: alice invites you to join the group as admin" - bob <## "use /j club to accept" + bob <## "#secret_club: alice invites you to join the group as admin" + bob <## "use /j secret_club to accept" ] - -- since bob is connected incognito to host, he uses different profile when joining group even though he turned incognito mode off - bob #$> ("/incognito off", id, "ok") - bob ##> "/j club" - bobMemIncognito <- getTermLine bob + bob ##> "/j secret_club" concurrently_ - (alice <## ("#club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito)) - (bob <## ("#club: you joined the group incognito as " <> bobMemIncognito)) - -- cath joins incognito - alice ##> "/a club cath" + (alice <## "#secret_club: bob joined the group") + (bob <## "#secret_club: you joined the group") + -- alice invites cath + alice ##> ("/a secret_club " <> cathIncognito) concurrentlyN_ - [ alice <## "invitation to join the group #club sent to cath", + [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito), do - cath <## "#club: alice invites you to join the group as admin" - cath <## "use /j club to accept" + cath <## "#secret_club: alice invites you to join the group as admin" + cath <## ("use /j secret_club to join incognito as " <> cathIncognito) ] - cath #$> ("/incognito on", id, "ok") - cath ##> "/j club" - cathMemIncognito <- getTermLine cath - -- bob and cath don't merge contacts + -- cath uses the same incognito profile when joining group, disabling incognito mode doesn't affect it + cath #$> ("/incognito off", id, "ok") + cath ##> "/j secret_club" + -- cath and bob don't merge contacts concurrentlyN_ - [ alice <## ("#club: cath joined the group incognito as " <> cathMemIncognito), + [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"), do - cath <## ("#club: you joined the group incognito as " <> cathMemIncognito) - cath <## ("#club: member " <> bobMemIncognito <> " is connected"), + cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito) + cath <## "#secret_club: member bob_1 (Bob) is connected", do - bob <## ("#club: alice added " <> cathMemIncognito <> " to the group (connecting...)") - bob <## ("#club: new member " <> cathMemIncognito <> " is connected") + bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)") + bob <## ("#secret_club: new member " <> cathIncognito <> " is connected") ] - -- cath invites dan incognito - cath ##> "/a club dan" + -- cath cannot invite to the group because her membership is incognito + cath ##> "/a secret_club dan" + cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" + -- alice invites dan + alice ##> "/a secret_club dan" concurrentlyN_ - [ cath <## "invitation to join the group #club incognito sent to dan", + [ alice <## "invitation to join the group #secret_club sent to dan", do - dan <## ("#club: cath (known to the group as " <> cathMemIncognito <> ") invites you to join the group incognito as admin") - dan <## "use /j club to join this group incognito" + dan <## "#secret_club: alice invites you to join the group as admin" + dan <## "use /j secret_club to accept" ] - dan ##> "/j club" - danMemIncognito <- getTermLine dan + dan ##> "/j secret_club" + -- cath and dan don't merge contacts concurrentlyN_ - [ cath <## ("#club: dan joined the group incognito as " <> danMemIncognito), + [ alice <## "#secret_club: dan joined the group", do - dan <## ("#club: you joined the group incognito as " <> danMemIncognito) + dan <## "#secret_club: you joined the group" dan - <### [ "#club: member alice (Alice) is connected", - "#club: member " <> bobMemIncognito <> " is connected" + <### [ "#secret_club: member " <> cathIncognito <> " is connected", + "#secret_club: member bob_1 (Bob) is connected", + "contact bob_1 is merged into bob", + "use @bob to send messages" ], do - alice <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)") - alice <## ("#club: new member " <> danMemIncognito <> " is connected"), + bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" + bob <## "#secret_club: new member dan_1 is connected" + bob <## "contact dan_1 is merged into dan" + bob <## "use @dan to send messages", do - bob <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)") - bob <## ("#club: new member " <> danMemIncognito <> " is connected") + cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" + cath <## "#secret_club: new member dan_1 is connected" ] - -- send messages - group is incognito for cath and dan - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") - cath #$> ("/incognito off", id, "ok") - dan #$> ("/incognito off", id, "ok") - alice #> "#club hello" + -- send messages - group is incognito for cath + alice #> "#secret_club hello" concurrentlyN_ - [ bob ?<# "#club alice> hello", - cath ?<# "#club alice> hello", - dan ?<# "#club alice> hello" + [ bob <# "#secret_club alice> hello", + cath ?<# "#secret_club alice> hello", + dan <# "#secret_club alice> hello" ] - bob ?#> "#club hi there" + bob #> "#secret_club hi there" concurrentlyN_ - [ alice <# ("#club " <> bobMemIncognito <> "> hi there"), - cath ?<# ("#club " <> bobMemIncognito <> "> hi there"), - dan ?<# ("#club " <> bobMemIncognito <> "> hi there") + [ alice <# "#secret_club bob> hi there", + cath ?<# "#secret_club bob_1> hi there", + dan <# "#secret_club bob> hi there" ] - cath ?#> "#club hey" + cath ?#> "#secret_club hey" concurrentlyN_ - [ alice <# ("#club " <> cathMemIncognito <> "> hey"), - bob ?<# ("#club " <> cathMemIncognito <> "> hey"), - dan ?<# ("#club " <> cathMemIncognito <> "> hey") + [ alice <# ("#secret_club " <> cathIncognito <> "> hey"), + bob <# ("#secret_club " <> cathIncognito <> "> hey"), + dan <# ("#secret_club " <> cathIncognito <> "> hey") ] - dan ?#> "#club how is it going?" + dan #> "#secret_club how is it going?" concurrentlyN_ - [ alice <# ("#club " <> danMemIncognito <> "> how is it going?"), - bob ?<# ("#club " <> danMemIncognito <> "> how is it going?"), - cath ?<# ("#club " <> danMemIncognito <> "> how is it going?") + [ alice <# "#secret_club dan> how is it going?", + bob <# "#secret_club dan> how is it going?", + cath ?<# "#secret_club dan_1> how is it going?" ] - -- bob and cath can send messages via direct incognito connections - bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob") - cath ?<# (bobMemIncognito <> "> hi, I'm bob") - cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath") - bob ?<# (cathMemIncognito <> "> hey, I'm cath") + -- cath and bob can send messages via new direct connection, cath is incognito + bob #> ("@" <> cathIncognito <> " hi, I'm bob") + cath ?<# "bob_1> hi, I'm bob" + cath ?#> "@bob_1 hey, I'm incognito" + bob <# (cathIncognito <> "> hey, I'm incognito") + -- cath and dan can send messages via new direct connection, cath is incognito + dan #> ("@" <> cathIncognito <> " hi, I'm dan") + cath ?<# "dan_1> hi, I'm dan" + cath ?#> "@dan_1 hey, I'm incognito" + dan <# (cathIncognito <> "> hey, I'm incognito") -- non incognito connections are separate bob <##> cath - -- bob and dan can send messages via direct incognito connections - bob ?#> ("@" <> danMemIncognito <> " hi, I'm bob") - dan ?<# (bobMemIncognito <> "> hi, I'm bob") - dan ?#> ("@" <> bobMemIncognito <> " hey, I'm dan") - bob ?<# (danMemIncognito <> "> hey, I'm dan") + dan <##> cath + -- list groups + cath ##> "/gs" + cath <## "i #secret_club" -- list group members - alice ##> "/ms club" + alice ##> "/ms secret_club" alice <### [ "alice (Alice): owner, you, created group", - "i " <> bobMemIncognito <> ": admin, invited, connected", - "i " <> cathMemIncognito <> ": admin, invited, connected", - danMemIncognito <> ": admin, connected" + "bob (Bob): admin, invited, connected", + cathIncognito <> ": admin, invited, connected", + "dan (Daniel): admin, invited, connected" ] - bob ##> "/ms club" + bob ##> "/ms secret_club" bob <### [ "alice (Alice): owner, host, connected", - "i " <> bobMemIncognito <> ": admin, you, connected", - cathMemIncognito <> ": admin, connected", - danMemIncognito <> ": admin, connected" + "bob (Bob): admin, you, connected", + cathIncognito <> ": admin, connected", + "dan (Daniel): admin, connected" ] - cath ##> "/ms club" + cath ##> "/ms secret_club" cath <### [ "alice (Alice): owner, host, connected", - bobMemIncognito <> ": admin, connected", - "i " <> cathMemIncognito <> ": admin, you, connected", - "i " <> danMemIncognito <> ": admin, invited, connected" + "bob_1 (Bob): admin, connected", + "i " <> cathIncognito <> ": admin, you, connected", + "dan_1 (Daniel): admin, connected" ] - dan ##> "/ms club" + dan ##> "/ms secret_club" dan - <### [ "alice (Alice): owner, connected", - bobMemIncognito <> ": admin, connected", - "i " <> cathMemIncognito <> ": admin, host, connected", - "i " <> danMemIncognito <> ": admin, you, connected" + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, connected", + cathIncognito <> ": admin, connected", + "dan (Daniel): admin, you, connected" ] + -- remove member + bob ##> ("/rm secret_club " <> cathIncognito) + concurrentlyN_ + [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), + alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), + dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), + do + cath <## "#secret_club: bob_1 removed you from the group" + cath <## "use /d #secret_club to delete the group" + ] + bob #> "#secret_club hi" + concurrentlyN_ + [ alice <# "#secret_club bob> hi", + dan <# "#secret_club bob> hi", + (cath "#secret_club hello" + concurrentlyN_ + [ bob <# "#secret_club alice> hello", + dan <# "#secret_club alice> hello", + (cath "#secret_club hello" + cath <## "you are no longer a member of the group" + -- cath can still message members directly + bob #> ("@" <> cathIncognito <> " I removed you from group") + cath ?<# "bob_1> I removed you from group" + cath ?#> "@bob_1 ok" + bob <# (cathIncognito <> "> ok") -testCantInviteIncognitoConnectionNonIncognitoGroup :: IO () -testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobProfile $ +testCantInviteContactIncognito :: IO () +testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- alice connected incognito to bob alice #$> ("/incognito on", id, "ok") @@ -2439,7 +2359,9 @@ testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobP alice <## "group #club is created" alice <## "use /a club to add members" alice ##> "/a club bob" - alice <## "you're using main profile for this group - prohibited to invite contact to whom you are connected incognito" + alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito" + -- bob doesn't receive invitation + (bob Date: Sun, 28 Aug 2022 09:14:55 +0100 Subject: [PATCH 57/89] ios: dark/light mode toggle (#975) --- apps/ios/Shared/AppDelegate.swift | 1 + .../UserSettings/AppearanceSettings.swift | 33 +++++++++++++++++++ .../Views/UserSettings/SettingsView.swift | 4 ++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 068693461b..b79d9bbbe3 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -107,5 +107,6 @@ class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } window = windowScene.keyWindow window?.tintColor = UIColor(cgColor: getUIAccentColorDefault()) + window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault() } } diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 98aa97fec7..aaa7d95268 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -10,10 +10,15 @@ import SwiftUI let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1) +let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark] + +let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"] + struct AppearanceSettings: View { @EnvironmentObject var sceneDelegate: SceneDelegate @State private var iconLightTapped = false @State private var iconDarkTapped = false + @State private var userInterfaceStyle = getUserInterfaceStyleDefault() @State private var uiTintColor = getUIAccentColorDefault() var body: some View { @@ -28,6 +33,11 @@ struct AppearanceSettings: View { } Section { + Picker("Theme", selection: $userInterfaceStyle) { + ForEach(interfaceStyles, id: \.self) { style in + Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0]) + } + } ColorPicker("Accent color", selection: $uiTintColor, supportsOpacity: false) } header: { Text("Colors") @@ -39,6 +49,10 @@ struct AppearanceSettings: View { Text("Reset colors").font(.callout) } } + .onChange(of: userInterfaceStyle) { _ in + sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle + setUserInterfaceStyleDefault(userInterfaceStyle) + } .onChange(of: uiTintColor) { _ in sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor) setUIAccentColorDefault(uiTintColor) @@ -84,6 +98,25 @@ func setUIAccentColorDefault(_ color: CGColor) { } } +func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle { + switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) { + case 1: return .light + case 2: return .dark + default: return .unspecified + } +} + +func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) { + var v: Int + switch style { + case .unspecified: v = 0 + case .light: v = 1 + case .dark: v = 2 + default: v = 0 + } + UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE) +} + struct AppearanceSettings_Previews: PreviewProvider { static var previews: some View { AppearanceSettings() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 4177e4496b..053b578d31 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -29,6 +29,7 @@ let DEFAULT_DEVELOPER_TOOLS = "developerTools" let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" +let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, @@ -42,7 +43,8 @@ let appDefaults: [String: Any] = [ DEFAULT_DEVELOPER_TOOLS: false, DEFAULT_ACCENT_COLOR_RED: 0.000, DEFAULT_ACCENT_COLOR_GREEN: 0.533, - DEFAULT_ACCENT_COLOR_BLUE: 1.000 + DEFAULT_ACCENT_COLOR_BLUE: 1.000, + DEFAULT_USER_INTERFACE_STYLE: 0 ] private var indent: CGFloat = 36 From 7343e4a51aa3fb8078d518ec02641f45712390fd Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:47:29 +0400 Subject: [PATCH 58/89] ios: simplify incognito feature (#979) --- apps/ios/Shared/Model/SimpleXAPI.swift | 4 +- .../Chat/ChatItem/CIGroupInvitationView.swift | 19 +-- apps/ios/Shared/Views/Chat/ChatView.swift | 29 ++-- .../Chat/Group/AddGroupMembersView.swift | 49 ++----- .../Views/Chat/Group/GroupChatInfoView.swift | 33 +++-- .../Chat/Group/GroupMemberInfoView.swift | 18 --- .../Views/ChatList/ChatListNavLink.swift | 37 +---- .../Views/ChatList/ChatPreviewView.swift | 17 ++- .../Shared/Views/NewChat/AddGroupView.swift | 13 +- .../Views/UserSettings/IncognitoHelp.swift | 29 +--- .../en.xcloc/Localized Contents/en.xliff | 130 ++++------------- .../ru.xcloc/Localized Contents/ru.xliff | 132 ++++-------------- apps/ios/SimpleXChat/APITypes.swift | 4 +- apps/ios/SimpleXChat/ChatTypes.swift | 14 +- apps/ios/ru.lproj/Localizable.strings | 70 ++-------- 15 files changed, 165 insertions(+), 433 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ea5df407da..9f17dc8737 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -324,9 +324,9 @@ func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile throw r } -func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?, LocalProfile?) { +func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?) { let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .groupMemberInfo(_, _, connStats_, localMainProfile) = r { return (connStats_, localMainProfile) } + if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) } throw r } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 8fc5421f52..c7ec3ca713 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -20,7 +20,6 @@ struct CIGroupInvitationView: View { var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending - let unsafeToJoinIncognito = interactiveIncognito && !chatIncognito let v = ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading) { groupInfoView(action) @@ -34,8 +33,8 @@ struct CIGroupInvitationView: View { if action { groupInvitationText() .overlay(DetermineWidth()) - Text(interactiveIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(interactiveIncognito ? .indigo : .accentColor) + Text(chatIncognito ? "Tap to join incognito" : "Tap to join") + .foregroundColor(chatIncognito ? .indigo : .accentColor) .font(.callout) .padding(.trailing, 60) .overlay(DetermineWidth()) @@ -59,25 +58,17 @@ struct CIGroupInvitationView: View { if action { v.onTapGesture { - if unsafeToJoinIncognito { - AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(groupInvitation.groupId)) - } else { - joinGroup(groupInvitation.groupId) - } + joinGroup(groupInvitation.groupId) } } else { v } } - private var interactiveIncognito: Bool { - (groupInvitation.invitedIncognito ?? false) || chatModel.incognito - } - private func groupInfoView(_ action: Bool) -> some View { var color: Color if action { - color = interactiveIncognito ? .indigo : .accentColor + color = chatIncognito ? .indigo : .accentColor } else { color = Color(uiColor: .tertiaryLabel) } @@ -107,7 +98,7 @@ struct CIGroupInvitationView: View { private func groupInvitationStr() -> LocalizedStringKey { if chatItem.chatDir.sent { - return (groupInvitation.invitedIncognito ?? false) ? "You sent group invitation incognito" : "You sent group invitation" + return "You sent group invitation" } else { switch groupInvitation.status { case .pending: return "You are invited to group" diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 42dfb7b676..5251617021 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -34,7 +34,6 @@ struct ChatView: View { // opening GroupMemberInfoView on member icon @State private var selectedMember: GroupMember? = nil @State private var memberConnectionStats: ConnectionStats? - @State private var memberMainProfile: LocalProfile? var body: some View { let cInfo = chat.chatInfo @@ -137,10 +136,16 @@ struct ChatView: View { case let .group(groupInfo): HStack { if groupInfo.canAddMembers { - addMembersButton() - .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) - } + if (chat.chatInfo.incognito) { + Image(systemName: "person.crop.circle.badge.plus") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .onTapGesture { AlertManager.shared.showAlert(cantInviteIncognitoAlert()) } + } else { + addMembersButton() + .sheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + } + } } Menu { searchButton() @@ -360,22 +365,16 @@ struct ChatView: View { .onTapGesture { Task { do { - let (stats, profile) = try await apiGroupMemberInfo(member.groupId, member.groupMemberId) - await MainActor.run { - memberConnectionStats = stats - memberMainProfile = profile - } + let stats = try await apiGroupMemberInfo(member.groupId, member.groupMemberId) + await MainActor.run { memberConnectionStats = stats } } catch let error { logger.error("apiGroupMemberInfo error: \(responseError(error))") } await MainActor.run { selectedMember = member } } } - .sheet(item: $selectedMember, onDismiss: { - memberConnectionStats = nil - memberMainProfile = nil - }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats, mainProfile: memberMainProfile) + .sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats) } } else { Rectangle().fill(.clear) diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 6a75758e02..2ab0fc5161 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -15,6 +15,7 @@ struct AddGroupMembersView: View { var chat: Chat var groupInfo: GroupInfo var showSkip: Bool = false + var showFooterCounter: Bool = true var addedMembersCb: ((Set) -> Void)? = nil @State private var selectedContacts = Set() @State private var selectedRole: GroupMemberRole = .admin @@ -22,13 +23,11 @@ struct AddGroupMembersView: View { private enum AddGroupMembersAlert: Identifiable { case prohibitedToInviteIncognito - case warnUnsafeToInviteIncognito case error(title: LocalizedStringKey, error: String = "") var id: String { switch self { case .prohibitedToInviteIncognito: return "prohibitedToInviteIncognito" - case .warnUnsafeToInviteIncognito: return "warnUnsafeToInviteIncognito" case let .error(title, _): return "error \(title)" } } @@ -37,10 +36,6 @@ struct AddGroupMembersView: View { var body: some View { NavigationView { let membersToAdd = filterMembersToAdd(chatModel.groupMembers) - let nonIncognitoConnectionsSelected = membersToAdd - .filter{ selectedContacts.contains($0.apiId) } - .contains(where: { !$0.contactConnIncognito }) - let unsafeToInviteIncognito = chat.chatInfo.incognito && nonIncognitoConnectionsSelected let v = List { ChatInfoToolbar(chat: chat, imageSize: 48) @@ -58,18 +53,20 @@ struct AddGroupMembersView: View { let count = selectedContacts.count Section { rolePicker() - inviteMembersButton(unsafeToInviteIncognito) + inviteMembersButton() .disabled(count < 1) } footer: { - if (count >= 1) { - HStack { - Button { selectedContacts.removeAll() } label: { Text("Clear") } - Spacer() - Text("\(count) contact(s) selected") + if showFooterCounter { + if (count >= 1) { + HStack { + Button { selectedContacts.removeAll() } label: { Text("Clear") } + Spacer() + Text("\(count) contact(s) selected") + } + } else { + Text("No contacts selected") + .frame(maxWidth: .infinity, alignment: .trailing) } - } else { - Text("No contacts selected") - .frame(maxWidth: .infinity, alignment: .trailing) } } @@ -103,27 +100,15 @@ struct AddGroupMembersView: View { title: Text("Can't invite contact!"), message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile") ) - case .warnUnsafeToInviteIncognito: - return Alert( - title: Text("Your main profile may be shared"), - message: Text("Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."), - primaryButton: .destructive(Text("Invite anyway")) { - inviteMembers() - }, secondaryButton: .cancel() - ) case let .error(title, error): return Alert(title: Text(title), message: Text("\(error)")) } } } - private func inviteMembersButton(_ unsafeToInviteIncognito: Bool) -> some View { + private func inviteMembersButton() -> some View { Button { - if unsafeToInviteIncognito { - alert = .warnUnsafeToInviteIncognito - } else { - inviteMembers() - } + inviteMembers() } label: { HStack { Text("Invite to group") @@ -161,7 +146,6 @@ struct AddGroupMembersView: View { private func contactCheckView(_ contact: Contact) -> some View { let checked = selectedContacts.contains(contact.apiId) let prohibitedToInviteIncognito = !chat.chatInfo.incognito && contact.contactConnIncognito - let safeToInviteIncognito = chat.chatInfo.incognito && contact.contactConnIncognito var icon: String var iconColor: Color if prohibitedToInviteIncognito { @@ -195,11 +179,6 @@ struct AddGroupMembersView: View { .foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary) .lineLimit(1) Spacer() - if safeToInviteIncognito { - Image(systemName: "theatermasks") - .foregroundColor(.indigo) - .font(.footnote) - } Image(systemName: icon) .foregroundColor(iconColor) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 1ce709881e..25b9cff0e7 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -20,13 +20,13 @@ struct GroupChatInfoView: View { @State private var selectedMember: GroupMember? = nil @State private var showGroupProfile: Bool = false @State private var connectionStats: ConnectionStats? - @State private var memberMainProfile: LocalProfile? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum GroupChatInfoViewAlert: Identifiable { case deleteGroupAlert case clearChatAlert case leaveGroupAlert + case cantInviteIncognitoAlert var id: GroupChatInfoViewAlert { get { self } } } @@ -43,18 +43,21 @@ struct GroupChatInfoView: View { Section("\(members.count + 1) members") { if groupInfo.canAddMembers { - addMembersButton() + if (chat.chatInfo.incognito) { + Label("Invite members", systemImage: "plus") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .onTapGesture { alert = .cantInviteIncognitoAlert } + } else { + addMembersButton() + } } memberView(groupInfo.membership, user: true) ForEach(members) { member in Button { Task { do { - let (stats, profile) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - await MainActor.run { - connectionStats = stats - memberMainProfile = profile - } + let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + await MainActor.run { connectionStats = stats } } catch let error { logger.error("apiGroupMemberInfo error: \(responseError(error))") } @@ -66,11 +69,8 @@ struct GroupChatInfoView: View { .sheet(isPresented: $showAddMembersSheet) { AddGroupMembersView(chat: chat, groupInfo: groupInfo) } - .sheet(item: $selectedMember, onDismiss: { - connectionStats = nil - memberMainProfile = nil - }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: memberMainProfile) + .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats) } .sheet(isPresented: $showGroupProfile) { GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) @@ -104,6 +104,8 @@ struct GroupChatInfoView: View { case .deleteGroupAlert: return deleteGroupAlert() case .clearChatAlert: return clearChatAlert() case .leaveGroupAlert: return leaveGroupAlert() + case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert() + } } } @@ -259,6 +261,13 @@ struct GroupChatInfoView: View { } } +func cantInviteIncognitoAlert() -> Alert { + Alert( + title: Text("Can't invite contacts!"), + message: Text("You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed") + ) +} + struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 5fa144bd75..3a315bb3e6 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -15,7 +15,6 @@ struct GroupMemberInfoView: View { var groupInfo: GroupInfo var member: GroupMember var connectionStats: ConnectionStats? - var mainProfile: LocalProfile? @State private var alert: GroupMemberInfoViewAlert? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -39,9 +38,6 @@ struct GroupMemberInfoView: View { Section("Member") { infoRow("Group", groupInfo.displayName) - if let mainProfile = mainProfile { - mainProfileRow(mainProfile) - } // TODO change role // localizedInfoRow("Role", member.memberRole.text) // TODO invited by - need to get contact by contact id @@ -99,20 +95,6 @@ struct GroupMemberInfoView: View { } } - private func mainProfileRow(_ mainProfile: LocalProfile) -> some View { - HStack { - Text("Known main profile") - Spacer() - if (mainProfile.image != nil) { - ProfileImage(imageStr: member.image) - .frame(width: 38, height: 38) - .padding(.trailing, 2) - } - Text(mainProfile.chatViewName) - .foregroundColor(.secondary) - } - } - private func groupMemberInfoHeader() -> some View { VStack { ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill)) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 416a7ca82d..6cfef1a403 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -82,12 +82,8 @@ struct ChatListNavLink: View { } .onTapGesture { showJoinGroupDialog = true } .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { - Button(interactiveIncognito ? "Join incognito" : "Join group") { - if unsafeToJoinIncognito(groupInfo.hostConnCustomUserProfileId) { - AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId)) - } else { - joinGroup(groupInfo.groupId) - } + Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") { + joinGroup(groupInfo.groupId) } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } } @@ -130,25 +126,13 @@ struct ChatListNavLink: View { } } - private var interactiveIncognito: Bool { - chat.chatInfo.incognito || chatModel.incognito - } - private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View { Button { - if unsafeToJoinIncognito(hostConnCustomUserProfileId) { - AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId)) - } else { - joinGroup(chat.chatInfo.apiId) - } + joinGroup(chat.chatInfo.apiId) } label: { - Label("Join", systemImage: interactiveIncognito ? "theatermasks" : "ipad.and.arrow.forward") + Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") } - .tint(interactiveIncognito ? .indigo : .accentColor) - } - - private func unsafeToJoinIncognito(_ hostConnCustomUserProfileId: Int64?) -> Bool { - interactiveIncognito && hostConnCustomUserProfileId == nil + .tint(chat.chatInfo.incognito ? .indigo : .accentColor) } private func markReadButton() -> some View { @@ -344,17 +328,6 @@ struct ChatListNavLink: View { } } -func unsafeToJoinIncognitoAlert(_ groupId: Int64) -> Alert { - Alert( - title: Text("Your main profile may be shared"), - message: Text("The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."), - primaryButton: .destructive(Text("Join anyway")) { - joinGroup(groupId) - }, - secondaryButton: .cancel() - ) -} - func joinGroup(_ groupId: Int64) { Task { logger.debug("joinGroup") diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 8d591bfc83..dd2e5a1469 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -89,7 +89,7 @@ struct ChatPreviewView: View { case .group(groupInfo: let groupInfo): switch (groupInfo.membership.memberStatus) { case .memInvited: - interactiveIncognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor) + chat.chatInfo.incognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor) case .memAccepted: v.foregroundColor(.secondary) default: v @@ -98,10 +98,6 @@ struct ChatPreviewView: View { } } - private var interactiveIncognito: Bool { - chat.chatInfo.incognito || chatModel.incognito - } - @ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View { if let cItem = cItem { ZStack(alignment: .topTrailing) { @@ -131,7 +127,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { - case .memInvited: chatPreviewInfoText(groupInfo.membership.memberIncognito ? "you are invited to group incognito" : "you are invited to group") + case .memInvited: groupInvitationPreviewText(groupInfo) case .memAccepted: chatPreviewInfoText("connecting…") default: EmptyView() } @@ -140,6 +136,15 @@ struct ChatPreviewView: View { } } + @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { + groupInfo.membership.memberIncognito + ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") + : (chatModel.incognito + ? chatPreviewInfoText("join as \(chatModel.currentUser?.profile.displayName ?? "yourself")") + : chatPreviewInfoText("you are invited to group") + ) + } + @ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { Text(text) .frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 11e6948672..ed4e82c0e0 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -24,9 +24,12 @@ struct AddGroupView: View { var body: some View { if let chat = chat, let groupInfo = groupInfo { - AddGroupMembersView(chat: chat, - groupInfo: groupInfo, - showSkip: true) { _ in + AddGroupMembersView( + chat: chat, + groupInfo: groupInfo, + showSkip: true, + showFooterCounter: false + ) { _ in dismiss() DispatchQueue.main.async { m.chatId = groupInfo.id @@ -46,9 +49,9 @@ struct AddGroupView: View { .padding(.bottom, 4) if (m.incognito) { HStack { - Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Image(systemName: "info.circle").foregroundColor(.orange).font(.footnote) Spacer().frame(width: 8) - Text("You will use a random profile for this group").font(.footnote) + Text("Incognito mode is not supported here - your main profile will be sent to group members").font(.footnote) } .padding(.bottom) } else { diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index 32c548ed47..92f0f8c201 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -15,34 +15,15 @@ struct IncognitoHelp: View { .font(.largeTitle) .padding(.vertical) ScrollView { - VStack(alignment: .leading, spacing: 24) { - Text("Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.") + VStack(alignment: .leading) { + Group { + Text("Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.") Text("It allows having many anonymous connections without any shared data between them in a single chat profile.") + Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.") Text("To find the profile used for an incognito connection, tap the contact or group name on top of the chat.") - } - .padding(.bottom) - - VStack(alignment: .leading, spacing: 8) { - Text("Incognito groups") - .font(.title2) - .padding(.top) - Text("When you join a group incognito, your new member profile is created and shared with all members.") - Group { - Text("Your are incognito in a group when:") - .padding(.top) - textListItem("•", "the group is created in Incognito mode,") - textListItem("•", "you or the member who invited you joined in Incognito mode,") - textListItem("•", "you have an incognito connection with the member who invited you.") - } - Group { - Text("Risks and limitations:") - .padding(.top) - textListItem("•", "It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.") - textListItem("•", "There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.") - } + .padding(.bottom) } - .padding(.bottom) } } .frame(maxWidth: .infinity) diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 54abdd9d69..7ef483a497 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -293,6 +293,11 @@ Can't invite contact! No comment provided by engineer. + + Can't invite contacts! + Can't invite contacts! + No comment provided by engineer. + Cancel Cancel @@ -1008,19 +1013,19 @@ Incognito No comment provided by engineer. - - Incognito groups - Incognito groups - No comment provided by engineer. - Incognito mode Incognito mode No comment provided by engineer. - - Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. - Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. + + Incognito mode is not supported here - your main profile will be sent to group members + Incognito mode is not supported here - your main profile will be sent to group members + No comment provided by engineer. + + + Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. + Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. No comment provided by engineer. @@ -1058,11 +1063,6 @@ Invitation expired! No comment provided by engineer. - - Invite anyway - Invite anyway - No comment provided by engineer. - Invite members Invite members @@ -1093,11 +1093,6 @@ Please connect to the developers via Settings to receive the updates about the s We will be adding server redundancy to prevent lost messages. No comment provided by engineer. - - It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. - It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. - No comment provided by engineer. - It seems like you are already connected via this link. If it is not the case, there was an error (%@). It seems like you are already connected via this link. If it is not the case, there was an error (%@). @@ -1113,11 +1108,6 @@ We will be adding server redundancy to prevent lost messages. Join No comment provided by engineer. - - Join anyway - Join anyway - No comment provided by engineer. - Join group Join group @@ -1133,11 +1123,6 @@ We will be adding server redundancy to prevent lost messages. Joining group No comment provided by engineer. - - Known main profile - Known main profile - No comment provided by engineer. - Large file! Large file! @@ -1558,11 +1543,6 @@ We will be adding server redundancy to prevent lost messages. Revert No comment provided by engineer. - - Risks and limitations: - Risks and limitations: - No comment provided by engineer. - Run chat Run chat @@ -1618,6 +1598,11 @@ We will be adding server redundancy to prevent lost messages. Search No comment provided by engineer. + + Send direct message + Send direct message + No comment provided by engineer. + Send link previews Send link previews @@ -1708,11 +1693,6 @@ We will be adding server redundancy to prevent lost messages. Skipped messages No comment provided by engineer. - - Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - No comment provided by engineer. - Somebody Somebody @@ -1808,11 +1788,6 @@ We will be adding server redundancy to prevent lost messages. The connection you accepted will be cancelled! No comment provided by engineer. - - The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - No comment provided by engineer. - The contact you shared this link with will NOT be able to connect! The contact you shared this link with will NOT be able to connect! @@ -1853,11 +1828,6 @@ We will be adding server redundancy to prevent lost messages. The sender will NOT be notified No comment provided by engineer. - - There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. - There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. - No comment provided by engineer. - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. @@ -2037,9 +2007,9 @@ To connect, please ask your contact to create another connection link and check When available No comment provided by engineer. - - When you join a group incognito, your new member profile is created and shared with all members. - When you join a group incognito, your new member profile is created and shared with all members. + + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. No comment provided by engineer. @@ -2137,11 +2107,6 @@ To connect, please ask your contact to create another connection link and check You sent group invitation No comment provided by engineer. - - You sent group invitation incognito - You sent group invitation incognito - No comment provided by engineer. - You will be connected when your connection request is accepted, please wait or check later! You will be connected when your connection request is accepted, please wait or check later! @@ -2162,16 +2127,16 @@ To connect, please ask your contact to create another connection link and check You will stop receiving messages from this group. Chat history will be preserved. No comment provided by engineer. - - You will use a random profile for this group - You will use a random profile for this group - No comment provided by engineer. - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile No comment provided by engineer. + + You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed + You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed + No comment provided by engineer. + Your SMP servers Your SMP servers @@ -2182,11 +2147,6 @@ To connect, please ask your contact to create another connection link and check Your SimpleX contact address No comment provided by engineer. - - Your are incognito in a group when: - Your are incognito in a group when: - No comment provided by engineer. - Your calls Your calls @@ -2244,11 +2204,6 @@ You can cancel this connection and remove the contact (and try later with a new Your current chat database will be DELETED and REPLACED with the imported one. No comment provided by engineer. - - Your main profile may be shared - Your main profile may be shared - No comment provided by engineer. - Your privacy Your privacy @@ -2476,11 +2431,6 @@ SimpleX servers cannot see your profile. group profile updated snd group event chat item - - incognito invitation to group %@ - incognito invitation to group %@ - group name - incognito via contact address link incognito via contact address link @@ -2521,10 +2471,10 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - - known to you as %@ connected incognito - known to you as %@ connected incognito - rcv group event chat item + + join as %@ + join as %@ + No comment provided by engineer. left @@ -2626,11 +2576,6 @@ SimpleX servers cannot see your profile. strike No comment provided by engineer. - - the group is created in Incognito mode, - the group is created in Incognito mode, - No comment provided by engineer. - this contact this contact @@ -2691,26 +2636,11 @@ SimpleX servers cannot see your profile. you are invited to group No comment provided by engineer. - - you are invited to group incognito - you are invited to group incognito - No comment provided by engineer. - - - you have an incognito connection with the member who invited you. - you have an incognito connection with the member who invited you. - No comment provided by engineer. - you left you left snd group event chat item - - you or the member who invited you joined in Incognito mode, - you or the member who invited you joined in Incognito mode, - No comment provided by engineer. - you removed %@ you removed %@ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 7902c5b90c..a759506256 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -293,6 +293,11 @@ Нельзя пригласить контакт! No comment provided by engineer. + + Can't invite contacts! + Нельзя пригласить контакты! + No comment provided by engineer. + Cancel Отменить @@ -1008,19 +1013,19 @@ Инкогнито No comment provided by engineer. - - Incognito groups - Инкогнито группы - No comment provided by engineer. - Incognito mode Режим Инкогнито No comment provided by engineer. - - Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created. - Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль. + + Incognito mode is not supported here - your main profile will be sent to group members + Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы + No comment provided by engineer. + + + Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. + Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль. No comment provided by engineer. @@ -1058,11 +1063,6 @@ Приглашение истекло! No comment provided by engineer. - - Invite anyway - Пригласить - No comment provided by engineer. - Invite members Пригласить членов группы @@ -1093,11 +1093,6 @@ We will be adding server redundancy to prevent lost messages. Мы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения. No comment provided by engineer. - - It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile. - Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль. - No comment provided by engineer. - It seems like you are already connected via this link. If it is not the case, there was an error (%@). Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@). @@ -1113,11 +1108,6 @@ We will be adding server redundancy to prevent lost messages. Вступить No comment provided by engineer. - - Join anyway - Вступить - No comment provided by engineer. - Join group Вступить в группу @@ -1133,11 +1123,6 @@ We will be adding server redundancy to prevent lost messages. Вступление в группу No comment provided by engineer. - - Known main profile - Известный основной профиль - No comment provided by engineer. - Large file! Большой файл! @@ -1558,11 +1543,6 @@ We will be adding server redundancy to prevent lost messages. Отменить изменения No comment provided by engineer. - - Risks and limitations: - Риски и ограничения: - No comment provided by engineer. - Run chat Запустить chat @@ -1618,6 +1598,11 @@ We will be adding server redundancy to prevent lost messages. Поиск No comment provided by engineer. + + Send direct message + Отправить сообщение + No comment provided by engineer. + Send link previews Отправлять картинки ссылок @@ -1708,11 +1693,6 @@ We will be adding server redundancy to prevent lost messages. Пропущенные сообщения No comment provided by engineer. - - Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля. - No comment provided by engineer. - Somebody Контакт @@ -1808,11 +1788,6 @@ We will be adding server redundancy to prevent lost messages. Подтвержденное соединение будет отменено! No comment provided by engineer. - - The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members. - У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля. - No comment provided by engineer. - The contact you shared this link with will NOT be able to connect! Контакт, которому вы отправили эту ссылку, не сможет соединиться! @@ -1853,11 +1828,6 @@ We will be adding server redundancy to prevent lost messages. Отправитель не будет уведомлён No comment provided by engineer. - - There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown. - Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение. - No comment provided by engineer. - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. @@ -2037,9 +2007,9 @@ To connect, please ask your contact to create another connection link and check Когда возможно No comment provided by engineer. - - When you join a group incognito, your new member profile is created and shared with all members. - Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы. + + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. + Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. No comment provided by engineer. @@ -2137,11 +2107,6 @@ To connect, please ask your contact to create another connection link and check Вы отправили приглашение в группу No comment provided by engineer. - - You sent group invitation incognito - Вы отправили приглашение в группу инкогнито - No comment provided by engineer. - You will be connected when your connection request is accepted, please wait or check later! Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! @@ -2162,16 +2127,16 @@ To connect, please ask your contact to create another connection link and check Вы перестанете получать сообщения от этой группы. История чата будет сохранена. No comment provided by engineer. - - You will use a random profile for this group - Вы будете использовать случайный профиль для этой группы - No comment provided by engineer. - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль No comment provided by engineer. + + You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed + Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено + No comment provided by engineer. + Your SMP servers Ваши SMP серверы @@ -2182,11 +2147,6 @@ To connect, please ask your contact to create another connection link and check Ваш SimpleX адрес No comment provided by engineer. - - Your are incognito in a group when: - Вы инкогнито в группе, когда: - No comment provided by engineer. - Your calls Ваши звонки @@ -2209,7 +2169,7 @@ To connect, please ask your contact to create another connection link and check Your chat profile will be sent to group members - Ваш профиль чата будет отправлен участникам группы + Ваш профиль чата будет отправлен членам группы No comment provided by engineer. @@ -2244,11 +2204,6 @@ You can cancel this connection and remove the contact (and try later with a new Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. No comment provided by engineer. - - Your main profile may be shared - Вашим основным профилем могут поделиться - No comment provided by engineer. - Your privacy Конфиденциальность @@ -2476,11 +2431,6 @@ SimpleX серверы не могут получить доступ к ваше профиль группы обновлен snd group event chat item - - incognito invitation to group %@ - инкогнито приглашение в группу %@ - group name - incognito via contact address link инкогнито через ссылку-контакт @@ -2521,10 +2471,10 @@ SimpleX серверы не могут получить доступ к ваше курсив No comment provided by engineer. - - known to you as %@ connected incognito - известный(ая) вам как %@ соединен(а) инкогнито - rcv group event chat item + + join as %@ + вступить как %@ + No comment provided by engineer. left @@ -2626,11 +2576,6 @@ SimpleX серверы не могут получить доступ к ваше зачеркнуть No comment provided by engineer. - - the group is created in Incognito mode, - группа создана в режиме Инкогнито, - No comment provided by engineer. - this contact этот контакт @@ -2691,26 +2636,11 @@ SimpleX серверы не могут получить доступ к ваше вы приглашены в группу No comment provided by engineer. - - you are invited to group incognito - вы приглашены в группу инкогнито - No comment provided by engineer. - - - you have an incognito connection with the member who invited you. - вы соединены инкогнито с членом группы, который вас пригласил. - No comment provided by engineer. - you left вы покинули группу snd group event chat item - - you or the member who invited you joined in Incognito mode, - вы или пригласивший вас член группы вступили в группу в режиме Инкогнито, - No comment provided by engineer. - you removed %@ вы удалили %@ diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 060c55ccd0..2299bd899a 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -232,7 +232,7 @@ public enum ChatResponse: Decodable, Error { case userSMPServers(smpServers: [String]) case networkConfig(networkConfig: NetCfg) case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) - case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?, localMainProfile: LocalProfile?) + case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) case invitation(connReqInvitation: String) case sentConfirmation case sentInvitation @@ -416,7 +416,7 @@ public enum ChatResponse: Decodable, Error { case let .userSMPServers(smpServers): return String(describing: smpServers) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))" - case let .groupMemberInfo(groupInfo, member, connectionStats_, localMainProfile): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))\nlocalMainProfile: \(String(describing: localMainProfile))" + case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))" case let .invitation(connReqInvitation): return connReqInvitation case .sentConfirmation: return noDetails case .sentInvitation: return noDetails diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 72664dabd3..1808c0d5e7 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1390,12 +1390,9 @@ public struct CIGroupInvitation: Decodable { public var localDisplayName: GroupName public var groupProfile: GroupProfile public var status: CIGroupInvitationStatus - public var invitedIncognito: Bool? var text: String { - (invitedIncognito ?? false) ? - String.localizedStringWithFormat(NSLocalizedString("incognito invitation to group %@", comment: "group name"), groupProfile.displayName) - : String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName) + String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName) } public static func getSample(groupId: Int64 = 1, groupMemberId: Int64 = 1, localDisplayName: GroupName = "team", groupProfile: GroupProfile = GroupProfile.sampleData, status: CIGroupInvitationStatus = .pending) -> CIGroupInvitation { @@ -1412,7 +1409,7 @@ public enum CIGroupInvitationStatus: String, Decodable { public enum RcvGroupEvent: Decodable { case memberAdded(groupMemberId: Int64, profile: Profile) - case memberConnected(contactMainProfile: Profile?) + case memberConnected case memberLeft case memberDeleted(groupMemberId: Int64, profile: Profile) case userDeleted @@ -1423,12 +1420,7 @@ public enum RcvGroupEvent: Decodable { switch self { case let .memberAdded(_, profile): return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) - case let .memberConnected(contactMainProfile): - if let contactMainProfile = contactMainProfile { - return String.localizedStringWithFormat(NSLocalizedString("known to you as %@ connected incognito", comment: "rcv group event chat item"), contactMainProfile.profileViewName) - } else { - return NSLocalizedString("member connected", comment: "rcv group event chat item") - } + case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 23da2d5727..64a8b5b201 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -212,6 +212,9 @@ /* No comment provided by engineer. */ "Can't invite contact!" = "Нельзя пригласить контакт!"; +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Нельзя пригласить контакты!"; + /* No comment provided by engineer. */ "Cancel" = "Отменить"; @@ -719,17 +722,14 @@ /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; -/* No comment provided by engineer. */ -"Incognito groups" = "Инкогнито группы"; - -/* group name */ -"incognito invitation to group %@" = "инкогнито приглашение в группу %@"; - /* No comment provided by engineer. */ "Incognito mode" = "Режим Инкогнито"; /* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль."; +"Incognito mode is not supported here - your main profile will be sent to group members" = "Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы"; + +/* No comment provided by engineer. */ +"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль."; /* chat list item description */ "incognito via contact address link" = "инкогнито через ссылку-контакт"; @@ -764,9 +764,6 @@ /* group name */ "invitation to group %@" = "приглашение в группу %@"; -/* No comment provided by engineer. */ -"Invite anyway" = "Пригласить"; - /* No comment provided by engineer. */ "Invite members" = "Пригласить членов группы"; @@ -788,9 +785,6 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages." = "Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения."; -/* No comment provided by engineer. */ -"It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile." = "Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль."; - /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -804,7 +798,7 @@ "Join" = "Вступить"; /* No comment provided by engineer. */ -"Join anyway" = "Вступить"; +"join as %@" = "вступить как %@"; /* No comment provided by engineer. */ "Join group" = "Вступить в группу"; @@ -815,12 +809,6 @@ /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; -/* No comment provided by engineer. */ -"Known main profile" = "Известный основной профиль"; - -/* rcv group event chat item */ -"known to you as %@ connected incognito" = "известный(ая) вам как %@ соединен(а) инкогнито"; - /* No comment provided by engineer. */ "Large file!" = "Большой файл!"; @@ -1121,9 +1109,6 @@ /* No comment provided by engineer. */ "Revert" = "Отменить изменения"; -/* No comment provided by engineer. */ -"Risks and limitations:" = "Риски и ограничения:"; - /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; @@ -1157,6 +1142,9 @@ /* No comment provided by engineer. */ "secret" = "секрет"; +/* No comment provided by engineer. */ +"Send direct message" = "Отправить сообщение"; + /* No comment provided by engineer. */ "Send link previews" = "Отправлять картинки ссылок"; @@ -1217,9 +1205,6 @@ /* No comment provided by engineer. */ "SMP servers (one per line)" = "SMP серверы (один на строке)"; -/* No comment provided by engineer. */ -"Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля."; - /* notification title */ "Somebody" = "Контакт"; @@ -1283,18 +1268,12 @@ /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!"; -/* No comment provided by engineer. */ -"The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля."; - /* No comment provided by engineer. */ "The contact you shared this link with will NOT be able to connect!" = "Контакт, которому вы отправили эту ссылку, не сможет соединиться!"; /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "Созданный архив доступен через Настройки приложения."; -/* No comment provided by engineer. */ -"the group is created in Incognito mode," = "группа создана в режиме Инкогнито,"; - /* No comment provided by engineer. */ "The group is fully decentralized – it is visible only to the members." = "Группа полностью децентрализована — она видна только членам."; @@ -1313,9 +1292,6 @@ /* No comment provided by engineer. */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; -/* No comment provided by engineer. */ -"There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown." = "Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение."; - /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; @@ -1455,7 +1431,7 @@ "When available" = "Когда возможно"; /* No comment provided by engineer. */ -"When you join a group incognito, your new member profile is created and shared with all members." = "Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы."; +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; /* No comment provided by engineer. */ "You" = "Вы"; @@ -1475,9 +1451,6 @@ /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; -/* No comment provided by engineer. */ -"you are invited to group incognito" = "вы приглашены в группу инкогнито"; - /* No comment provided by engineer. */ "You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**."; @@ -1502,9 +1475,6 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; -/* No comment provided by engineer. */ -"you have an incognito connection with the member who invited you." = "вы соединены инкогнито с членом группы, который вас пригласил."; - /* No comment provided by engineer. */ "You invited your contact" = "Вы пригласили ваш контакт"; @@ -1520,9 +1490,6 @@ /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, инача вы можете перестать получать сообщения от каких то контактов."; -/* No comment provided by engineer. */ -"you or the member who invited you joined in Incognito mode," = "вы или пригласивший вас член группы вступили в группу в режиме Инкогнито,"; - /* No comment provided by engineer. */ "You rejected group invitation" = "Вы отклонили приглашение в группу"; @@ -1532,9 +1499,6 @@ /* No comment provided by engineer. */ "You sent group invitation" = "Вы отправили приглашение в группу"; -/* No comment provided by engineer. */ -"You sent group invitation incognito" = "Вы отправили приглашение в группу инкогнито"; - /* chat list item description */ "you shared one-time link" = "вы создали ссылку"; @@ -1553,9 +1517,6 @@ /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена."; -/* No comment provided by engineer. */ -"You will use a random profile for this group" = "Вы будете использовать случайный профиль для этой группы"; - /* No comment provided by engineer. */ "you: " = "вы: "; @@ -1563,7 +1524,7 @@ "You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль"; /* No comment provided by engineer. */ -"Your are incognito in a group when:" = "Вы инкогнито в группе, когда:"; +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено"; /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; @@ -1578,7 +1539,7 @@ "Your chat profile" = "Ваш профиль"; /* No comment provided by engineer. */ -"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен участникам группы"; +"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен членам группы"; /* No comment provided by engineer. */ "Your chat profile will be sent to your contact" = "Ваш профиль будет отправлен вашему контакту"; @@ -1598,9 +1559,6 @@ /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; -/* No comment provided by engineer. */ -"Your main profile may be shared" = "Вашим основным профилем могут поделиться"; - /* No comment provided by engineer. */ "Your privacy" = "Конфиденциальность"; From 51a2fa8c2830319df78a9b95a7c69b113886d54c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:08:46 +0100 Subject: [PATCH 59/89] ios: programmatic navigation between list/chat (#980) * ios: programmatic navigation between list/chat * prevent chat info sheet from showing when switching conversation * add direct chat with member to model * set status to connected Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/ios/Shared/Model/ChatModel.swift | 10 ++++++ apps/ios/Shared/Model/SimpleXAPI.swift | 3 +- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 1 + apps/ios/Shared/Views/Chat/ChatView.swift | 14 ++++++++- .../Views/Chat/Group/GroupChatInfoView.swift | 1 + .../Chat/Group/GroupMemberInfoView.swift | 31 ++++++++++++------- .../Views/ChatList/ChatListNavLink.swift | 7 ----- .../Shared/Views/ChatList/ChatListView.swift | 19 ++++++++++++ .../Shared/Views/Helpers/NavLinkPlain.swift | 7 +---- 9 files changed, 66 insertions(+), 27 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 6949762925..ab1739846a 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -59,6 +59,16 @@ final class ChatModel: ObservableObject { chats.first(where: { $0.id == id }) } + func getContactChat(_ contactId: Int64) -> Chat? { + chats.first { chat in + if case let .direct(contact) = chat.chatInfo { + return contact.contactId == contactId + } else { + return false + } + } + } + private func getChatIndex(_ id: String) -> Int? { chats.firstIndex(where: { $0.id == id }) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9f17dc8737..6db4b01075 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -206,8 +206,9 @@ func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, sear func loadChat(chat: Chat, search: String = "") { do { let cInfo = chat.chatInfo - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) let m = ChatModel.shared + m.reversedChatItems = [] + let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) m.updateChatInfo(chat.chatInfo) m.reversedChatItems = chat.chatItems.reversed() } catch let error { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 62449f90a0..667ae0e597 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -216,6 +216,7 @@ struct ChatInfoView: View { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { chatModel.removeChat(chat.chatInfo.id) + chatModel.chatId = nil dismiss() } } catch let error { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 5251617021..63368ecf48 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -72,7 +72,10 @@ struct ChatView: View { } } } label: { - Image(systemName: "chevron.backward") + HStack(spacing: 0) { + Image(systemName: "chevron.backward") + Text("Chats") + } } } ToolbarItem(placement: .principal) { @@ -235,6 +238,15 @@ struct ChatView: View { .onChange(of: searchText) { _ in loadChat(chat: chat, search: searchText) } + .onChange(of: chatModel.chatId) { _ in + if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { + showChatInfoSheet = false + loadChat(chat: chat) + DispatchQueue.main.async { + scrollToBottom(proxy) + } + } + } } } .scaleEffect(x: 1, y: -1, anchor: .center) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 25b9cff0e7..6cb92a10ac 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -221,6 +221,7 @@ struct GroupChatInfoView: View { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { chatModel.removeChat(chat.chatInfo.id) + chatModel.chatId = nil dismiss() } } catch let error { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 3a315bb3e6..e68378f4c7 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -30,11 +30,11 @@ struct GroupMemberInfoView: View { groupMemberInfoHeader() .listRowBackground(Color.clear) -// if let contactId = member.memberContactId { -// Section { -// openDirectChatButton(contactId) -// } -// } + if let contactId = member.memberContactId { + Section { + openDirectChatButton(contactId) + } + } Section("Member") { infoRow("Group", groupInfo.displayName) @@ -80,18 +80,25 @@ struct GroupMemberInfoView: View { func openDirectChatButton(_ contactId: Int64) -> some View { Button { - if let i = chatModel.chats.firstIndex(where: { chat in - switch chat.chatInfo { - case let .direct(contact): return contact.contactId == contactId - default: return false + var chat = chatModel.getContactChat(contactId) + if chat == nil { + do { + chat = try apiGetChat(type: .direct, id: contactId) + if let chat = chat { + // TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend + chat.serverInfo = Chat.ServerInfo(networkStatus: .connected) + chatModel.addChat(chat) + } + } catch let error { + logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") } - }) { + } + if let chat = chat { dismissAllSheets(animated: true) - chatModel.chatId = chatModel.chats[i].chatInfo.id + chatModel.chatId = chat.id } } label: { Label("Send direct message", systemImage: "message") - .foregroundColor(.accentColor) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 6cfef1a403..562abfc746 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -28,16 +28,10 @@ struct ChatListNavLink: View { } } - private func chatView() -> some View { - ChatView(chat: chat) - .onAppear { loadChat(chat: chat) } - } - @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { let v = NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, - destination: { chatView() }, label: { ChatPreviewView(chat: chat) }, disabled: !contact.ready ) @@ -97,7 +91,6 @@ struct ChatListNavLink: View { NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, - destination: { chatView() }, label: { ChatPreviewView(chat: chat) }, disabled: !groupInfo.ready ) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 4a4dd4d658..4c504eda8a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -14,6 +14,7 @@ struct ChatListView: View { // not really used in this view @State private var showSettings = false @State private var searchText = "" + @State private var selectedChat: ChatId? var body: some View { let v = NavigationView { @@ -25,6 +26,7 @@ struct ChatListView: View { } } .onChange(of: chatModel.chatId) { _ in + selectedChat = chatModel.chatId if chatModel.chatId == nil, let chatId = chatModel.chatToTop { chatModel.chatToTop = nil chatModel.popChat(chatId) @@ -63,6 +65,15 @@ struct ChatListView: View { } } } + .background( + NavigationLink( + destination: chatView(selectedChat), + isActive: Binding( + get: { selectedChat != nil }, + set: { _, _ in selectedChat = nil } + ) + ) { EmptyView() } + ) } .navigationViewStyle(.stack) @@ -73,6 +84,14 @@ struct ChatListView: View { } } + @ViewBuilder private func chatView(_ chatId: ChatId?) -> some View { + if let chatId = chatId, let chat = chatModel.getChat(chatId) { + ChatView(chat: chat).onAppear { + loadChat(chat: chat) + } + } + } + private func filteredChats() -> [Chat] { let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase return s == "" diff --git a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift index fb12292b6a..3dde57a427 100644 --- a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift +++ b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift @@ -8,10 +8,9 @@ import SwiftUI -struct NavLinkPlain: View { +struct NavLinkPlain: View { @State var tag: V @Binding var selection: V? - @ViewBuilder var destination: () -> Destination @ViewBuilder var label: () -> Label var disabled = false @@ -21,10 +20,6 @@ struct NavLinkPlain: View { .disabled(disabled) label() } - .background { - NavigationLink("", tag: tag, selection: $selection, destination: destination) - .hidden() - } } } From 14f6e5c16f09d488bfdeee5fe689da92cf96f41f Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Mon, 29 Aug 2022 20:10:27 +0400 Subject: [PATCH 60/89] ios: version 3.2 (70) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index eccc152754..622f881ee4 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1164,7 +1164,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1206,7 +1206,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1285,7 +1285,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1315,7 +1315,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1346,7 +1346,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1392,7 +1392,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; From 2d4348c50d32cb9c0228f5cf0fd7779c56f5b619 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:50:44 +0300 Subject: [PATCH 61/89] Regex for Emoji (#982) --- .../app/views/chat/item/EmojiItemView.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt index 8d6e59edc1..7ac5e0ca7e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt @@ -31,12 +31,19 @@ fun EmojiText(text: String) { Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont) } -private fun isSimpleEmoji(c: Int): Boolean = c > 0x238C +// https://stackoverflow.com/a/46279500 +private const val emojiStr = "^(" + + "(?:[\\u2700-\\u27bf]|" + + "(?:[\\ud83c\\udde6-\\ud83c\\uddff]){2}|" + + "[\\ud800\\udc00-\\uDBFF\\uDFFF]|[\\u2600-\\u26FF])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe23\\u20d0-\\u20f0]|[\\ud83c\\udffb-\\ud83c\\udfff])?" + + "(?:\\u200d(?:[^\\ud800-\\udfff]|" + + "(?:[\\ud83c\\udde6-\\ud83c\\uddff]){2}|" + + "[\\ud800\\udc00-\\uDBFF\\uDFFF]|[\\u2600-\\u26FF])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe23\\u20d0-\\u20f0]|[\\ud83c\\udffb-\\ud83c\\udfff])?)*|" + + "[\\u0023-\\u0039]\\ufe0f?\\u20e3|\\u3299|\\u3297|\\u303d|\\u3030|\\u24c2|[\\ud83c\\udd70-\\ud83c\\udd71]|[\\ud83c\\udd7e-\\ud83c\\udd7f]|\\ud83c\\udd8e|[\\ud83c\\udd91-\\ud83c\\udd9a]|[\\ud83c\\udde6-\\ud83c\\uddff]|[\\ud83c\\ude01-\\ud83c\\ude02]|\\ud83c\\ude1a|\\ud83c\\ude2f|[\\ud83c\\ude32-\\ud83c\\ude3a]|[\\ud83c\\ude50-\\ud83c\\ude51]|\\u203c|\\u2049|[\\u25aa-\\u25ab]|\\u25b6|\\u25c0|[\\u25fb-\\u25fe]|\\u00a9|\\u00ae|\\u2122|\\u2139|\\ud83c\\udc04|[\\u2600-\\u26FF]|\\u2b05|\\u2b06|\\u2b07|\\u2b1b|\\u2b1c|\\u2b50|\\u2b55|\\u231a|\\u231b|\\u2328|\\u23cf|[\\u23e9-\\u23f3]|[\\u23f8-\\u23fa]|\\ud83c\\udccf|\\u2934|\\u2935|[\\u2190-\\u21ff]" + + ")+$" // Multiple matches with emojis where one follows another without interruptions from other characters +private val emojiRegex = Regex(emojiStr) -fun isEmoji(c: Int): Boolean = isSimpleEmoji(c) // || isCombinedIntoEmoji(c) - -// TODO count perceived emojis, possibly using icu4j fun isShortEmoji(str: String): Boolean { val s = str.trim() - return s.codePoints().count() in 1..5 && s.codePoints().allMatch(::isEmoji) + return s.codePoints().count() in 1..5 && emojiRegex.matches(str) } From 0169589e7ce3edf5ac4e7402d759612585b8f599 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 30 Aug 2022 14:17:28 +0300 Subject: [PATCH 62/89] Incognito mode (#974) * Incognito mode * Incognito icon color and state applying * Added a spacer under username * Local contact aliases support * Simplified incognito * update help title * ChatInfo layout * corrections * color * icon Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 104 +++++++++++++---- .../java/chat/simplex/app/model/SimpleXAPI.kt | 32 ++++- .../java/chat/simplex/app/ui/theme/Color.kt | 1 + .../simplex/app/views/chat/ChatInfoView.kt | 80 ++++++++++++- .../chat/simplex/app/views/chat/ChatView.kt | 28 +++-- .../views/chat/group/AddGroupMembersView.kt | 59 ++++++++-- .../app/views/chat/group/GroupChatInfoView.kt | 47 ++++++-- .../views/chat/group/GroupMemberInfoView.kt | 8 +- .../views/chat/item/CIGroupInvitationView.kt | 19 ++- .../app/views/chat/item/ChatItemView.kt | 7 +- .../app/views/chatlist/ChatListNavLinkView.kt | 45 ++++--- .../app/views/chatlist/ChatListView.kt | 27 +++-- .../app/views/chatlist/ChatPreviewView.kt | 22 +++- .../app/views/chatlist/ContactRequestView.kt | 7 +- .../simplex/app/views/helpers/AlertManager.kt | 6 +- .../app/views/helpers/ChatInfoImage.kt | 14 ++- .../views/helpers/DefaultBasicTextField.kt | 110 ++++++++++++++++++ .../app/views/newchat/AddContactView.kt | 75 ++++++++++-- .../simplex/app/views/newchat/AddGroupView.kt | 18 ++- .../app/views/newchat/PasteToConnect.kt | 15 ++- .../app/views/newchat/ScanToConnectView.kt | 18 +-- .../app/views/usersettings/CallSettings.kt | 38 ++++++ .../app/views/usersettings/IncognitoView.kt | 47 ++++++++ .../app/views/usersettings/SettingsView.kt | 76 +++++++++++- .../app/views/usersettings/UserProfileView.kt | 9 +- .../app/src/main/res/values-ru/strings.xml | 34 +++++- .../app/src/main/res/values/strings.xml | 34 +++++- 27 files changed, 833 insertions(+), 147 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index e83f365cdd..918a6a30e4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -47,6 +47,7 @@ class ChatModel(val controller: ChatController) { val runServiceInBackground = mutableStateOf(true) val performLA = mutableStateOf(false) val showAdvertiseLAUnavailableAlert = mutableStateOf(false) + var incognito = mutableStateOf(false) // current WebRTC call val callManager = CallManager(this) @@ -57,7 +58,7 @@ class ChatModel(val controller: ChatController) { val showCallView = mutableStateOf(false) val switchingCall = mutableStateOf(false) - fun updateUserProfile(profile: Profile) { + fun updateUserProfile(profile: LocalProfile) { val user = currentUser.value if (user != null) { currentUser.value = user.copy(profile = profile) @@ -290,19 +291,20 @@ data class User( val userId: Long, val userContactId: Long, val localDisplayName: String, - val profile: Profile, + val profile: LocalProfile, val activeUser: Boolean ): NamedChat { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName override val image: String? get() = profile.image + override val localAlias: String = "" companion object { val sampleData = User( userId = 1, userContactId = 1, localDisplayName = "alice", - profile = Profile.sampleData, + profile = LocalProfile.sampleData, activeUser = true ) } @@ -314,8 +316,9 @@ interface NamedChat { val displayName: String val fullName: String val image: String? + val localAlias: String val chatViewName: String - get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") + get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } } interface SomeChat { @@ -376,6 +379,8 @@ data class Chat ( @Serializable sealed class ChatInfo: SomeChat, NamedChat { + abstract val incognito: Boolean + @Serializable @SerialName("direct") class Direct(val contact: Contact): ChatInfo() { override val chatType get() = ChatType.Direct @@ -385,11 +390,13 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contact.ready override val sendMsgEnabled get() = contact.sendMsgEnabled override val ntfsEnabled get() = contact.chatSettings.enableNtfs + override val incognito get() = contact.contactConnIncognito override val createdAt get() = contact.createdAt override val updatedAt get() = contact.updatedAt override val displayName get() = contact.displayName override val fullName get() = contact.fullName override val image get() = contact.image + override val localAlias: String get() = contact.localAlias companion object { val sampleData = Direct(Contact.sampleData) @@ -405,11 +412,13 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = groupInfo.ready override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val ntfsEnabled get() = groupInfo.chatSettings.enableNtfs + override val incognito get() = groupInfo.membership.memberIncognito override val createdAt get() = groupInfo.createdAt override val updatedAt get() = groupInfo.updatedAt override val displayName get() = groupInfo.displayName override val fullName get() = groupInfo.fullName override val image get() = groupInfo.image + override val localAlias get() = groupInfo.localAlias companion object { val sampleData = Group(GroupInfo.sampleData) @@ -425,11 +434,13 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contactRequest.ready override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val ntfsEnabled get() = false + override val incognito get() = false override val createdAt get() = contactRequest.createdAt override val updatedAt get() = contactRequest.updatedAt override val displayName get() = contactRequest.displayName override val fullName get() = contactRequest.fullName override val image get() = contactRequest.image + override val localAlias get() = contactRequest.localAlias companion object { val sampleData = ContactRequest(UserContactRequest.sampleData) @@ -445,11 +456,13 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contactConnection.ready override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val ntfsEnabled get() = false + override val incognito get() = contactConnection.incognito override val createdAt get() = contactConnection.createdAt override val updatedAt get() = contactConnection.updatedAt override val displayName get() = contactConnection.displayName override val fullName get() = contactConnection.fullName override val image get() = contactConnection.image + override val localAlias get() = contactConnection.localAlias companion object { fun getSampleData(status: ConnStatus = ConnStatus.New, viaContactUri: Boolean = false): ContactConnection = @@ -462,7 +475,7 @@ sealed class ChatInfo: SomeChat, NamedChat { data class Contact( val contactId: Long, override val localDisplayName: String, - val profile: Profile, + val profile: LocalProfile, val activeConn: Connection, val viaGroup: Long? = null, val chatSettings: ChatSettings, @@ -475,18 +488,22 @@ data class Contact( override val ready get() = activeConn.connStatus == ConnStatus.Ready override val sendMsgEnabled get() = true override val ntfsEnabled get() = chatSettings.enableNtfs - override val displayName get() = profile.displayName + override val displayName get() = localAlias.ifEmpty { profile.displayName } override val fullName get() = profile.fullName override val image get() = profile.image + override val localAlias get() = profile.localAlias val isIndirectContact: Boolean get() = activeConn.connLevel > 0 || viaGroup != null + val contactConnIncognito = + activeConn.customUserProfileId != null + companion object { val sampleData = Contact( contactId = 1, localDisplayName = "alice", - profile = Profile.sampleData, + profile = LocalProfile.sampleData, activeConn = Connection.sampleData, chatSettings = ChatSettings(true), createdAt = Clock.System.now(), @@ -510,10 +527,10 @@ class ContactSubStatus( ) @Serializable -class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int) { +class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val customUserProfileId: Long? = null) { val id: ChatId get() = ":$connId" companion object { - val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0) + val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, customUserProfileId = null) } } @@ -521,13 +538,16 @@ class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: In class Profile( override val displayName: String, override val fullName: String, - override val image: String? = null + override val image: String? = null, + override val localAlias : String = "" ): NamedChat { val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, image, localAlias) + companion object { val sampleData = Profile( displayName = "alice", @@ -536,6 +556,28 @@ class Profile( } } +@Serializable +class LocalProfile( + val profileId: Long, + override val displayName: String, + override val fullName: String, + override val image: String? = null, + override val localAlias: String, +): NamedChat { + val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } + + fun toProfile(): Profile = Profile(displayName, fullName, image, localAlias) + + companion object { + val sampleData = LocalProfile( + profileId = 1L, + displayName = "alice", + fullName = "Alice", + localAlias = "" + ) + } +} + @Serializable class Group ( val groupInfo: GroupInfo, @@ -548,6 +590,7 @@ data class GroupInfo ( override val localDisplayName: String, val groupProfile: GroupProfile, val membership: GroupMember, + val hostConnCustomUserProfileId: Long? = null, val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant @@ -561,6 +604,7 @@ data class GroupInfo ( override val displayName get() = groupProfile.displayName override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image + override val localAlias get() = "" val canEdit: Boolean get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent @@ -577,6 +621,7 @@ data class GroupInfo ( localDisplayName = "team", groupProfile = GroupProfile.sampleData, membership = GroupMember.sampleData, + hostConnCustomUserProfileId = null, chatSettings = ChatSettings(true), createdAt = Clock.System.now(), updatedAt = Clock.System.now() @@ -588,7 +633,8 @@ data class GroupInfo ( class GroupProfile ( override val displayName: String, override val fullName: String, - override val image: String? = null + override val image: String? = null, + override val localAlias: String = "", ): NamedChat { companion object { val sampleData = GroupProfile( @@ -608,17 +654,18 @@ class GroupMember ( var memberStatus: GroupMemberStatus, var invitedBy: InvitedBy, val localDisplayName: String, - val memberProfile: Profile, + val memberProfile: LocalProfile, val memberContactId: Long? = null, + val memberContactProfileId: Long, var activeConn: Connection? = null ) { val id: String get() = "#$groupId @$groupMemberId" - val displayName: String get() = memberProfile.displayName + val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName } val fullName: String get() = memberProfile.fullName val image: String? get() = memberProfile.image val chatViewName: String - get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") + get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } val memberActive: Boolean get() = when (this.memberStatus) { GroupMemberStatus.MemRemoved -> false @@ -654,6 +701,8 @@ class GroupMember ( && userRole >= GroupMemberRole.Admin && userRole >= memberRole && membership.memberCurrent } + val memberIncognito = memberProfile.profileId != memberContactProfileId + companion object { val sampleData = GroupMember( groupMemberId = 1, @@ -664,8 +713,9 @@ class GroupMember ( memberStatus = GroupMemberStatus.MemComplete, invitedBy = InvitedBy.IBUser(), localDisplayName = "alice", - memberProfile = Profile.sampleData, + memberProfile = LocalProfile.sampleData, memberContactId = 1, + memberContactProfileId = 1L, activeConn = Connection.sampleData ) } @@ -783,6 +833,7 @@ class UserContactRequest ( override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image + override val localAlias get() = "" companion object { val sampleData = UserContactRequest( @@ -801,6 +852,7 @@ class PendingContactConnection( val pccAgentConnId: String, val pccConnStatus: ConnStatus, val viaContactUri: Boolean, + val customUserProfileId: Long? = null, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -825,14 +877,21 @@ class PendingContactConnection( } override val fullName get() = "" override val image get() = null + override val localAlias get() = "" + val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri + val incognito = customUserProfileId != null + val description: String get() { val initiated = pccConnStatus.initiated return if (initiated == null) "" else generalGetString( - if (initiated && !viaContactUri) R.string.description_you_shared_one_time_link - else if (viaContactUri ) R.string.description_via_contact_address_link - else R.string.description_via_one_time_link + if (initiated && !viaContactUri) + if (incognito) R.string.description_you_shared_one_time_link_incognito else R.string.description_you_shared_one_time_link + else if (viaContactUri ) + if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link + else + if (incognito) R.string.description_via_one_time_link_incognito else R.string.description_via_one_time_link ) } @@ -843,6 +902,7 @@ class PendingContactConnection( pccAgentConnId = "abcd", pccConnStatus = status, viaContactUri = viaContactUri, + customUserProfileId = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now() ) @@ -897,7 +957,7 @@ data class ChatItem ( val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew val memberDisplayName: String? get() = - if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.memberProfile.displayName + if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName else null val isDeletedContent: Boolean get() = @@ -1190,9 +1250,11 @@ class CIGroupInvitation ( val groupMemberId: Long, val localDisplayName: String, val groupProfile: GroupProfile, - val status: CIGroupInvitationStatus + val status: CIGroupInvitationStatus, ) { - val text: String get() = String.format(generalGetString(R.string.group_invitation_item_description), groupProfile.displayName) + val text: String get() = String.format( + generalGetString(R.string.group_invitation_item_description), + groupProfile.displayName) companion object { fun getSample( diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 44f4e7f7c6..95da5ff799 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -95,6 +95,7 @@ class AppPreferences(val context: Context) { val networkTCPKeepIdle = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_IDLE, KeepAliveOpts.defaults.keepIdle) val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl) val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) + val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb()) @@ -165,6 +166,7 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_NETWORK_TCP_KEEP_IDLE = "NetworkTCPKeepIdle" private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" + private const val SHARED_PREFS_INCOGNITO = "Incognito" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor" } @@ -179,6 +181,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager init { chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get() chatModel.performLA.value = appPrefs.performLA.get() + chatModel.incognito.value = appPrefs.incognito.get() } suspend fun startChat(user: User) { @@ -189,6 +192,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager val justStarted = apiStartChat() if (justStarted) { apiSetFilesFolder(getAppFilesDirectory(appContext)) + apiSetIncognito(chatModel.incognito.value) chatModel.userAddress.value = apiGetUserAddress() chatModel.userSMPServers.value = getUserSMPServers() val chats = apiGetChats() @@ -297,6 +301,12 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("failed to set files folder: ${r.responseType} ${r.details}") } + suspend fun apiSetIncognito(incognito: Boolean) { + val r = sendCmd(CC.SetIncognito(incognito)) + if (r is CR.CmdOk) return + throw Exception("failed to set incognito: ${r.responseType} ${r.details}") + } + suspend fun apiExportArchive(config: ArchiveConfig) { val r = sendCmd(CC.ApiExportArchive(config)) if (r is CR.CmdOk) return @@ -405,9 +415,9 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun apiContactInfo(contactId: Long): ConnectionStats? { + suspend fun apiContactInfo(contactId: Long): Pair? { val r = sendCmd(CC.APIContactInfo(contactId)) - if (r is CR.ContactInfo) return r.connectionStats + if (r is CR.ContactInfo) return r.connectionStats to r.customUserProfile Log.e(TAG, "apiContactInfo bad response: ${r.responseType} ${r.details}") return null } @@ -518,6 +528,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return null } + suspend fun apiSetContactAlias(contactId: Long, localAlias: String): Contact? { + val r = sendCmd(CC.ApiSetContactAlias(contactId, localAlias)) + if (r is CR.ContactAliasUpdated) return r.toContact + Log.e(TAG, "apiSetContactAlias bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiCreateUserAddress(): String? { val r = sendCmd(CC.CreateMyAddress()) if (r is CR.UserContactLinkCreated) return r.connReqContact @@ -1133,6 +1150,7 @@ sealed class CC { class StartChat: CC() class ApiStopChat: CC() class SetFilesFolder(val filesFolder: String): CC() + class SetIncognito(val incognito: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() @@ -1163,6 +1181,7 @@ sealed class CC { class ListContacts: CC() class ApiUpdateProfile(val profile: Profile): CC() class ApiParseMarkdown(val text: String): CC() + class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class CreateMyAddress: CC() class DeleteMyAddress: CC() class ShowMyAddress: CC() @@ -1185,6 +1204,7 @@ sealed class CC { is StartChat -> "/_start" is ApiStopChat -> "/_stop" is SetFilesFolder -> "/_files_folder $filesFolder" + is SetIncognito -> "/incognito ${if (incognito) "on" else "off"}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" @@ -1214,6 +1234,7 @@ sealed class CC { is ListContacts -> "/contacts" is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}" is ApiParseMarkdown -> "/_parse $text" + is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is CreateMyAddress -> "/address" is DeleteMyAddress -> "/delete_address" is ShowMyAddress -> "/show_address" @@ -1237,6 +1258,7 @@ sealed class CC { is StartChat -> "startChat" is ApiStopChat -> "apiStopChat" is SetFilesFolder -> "setFilesFolder" + is SetIncognito -> "setIncognito" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" @@ -1266,6 +1288,7 @@ sealed class CC { is ListContacts -> "listContacts" is ApiUpdateProfile -> "updateProfile" is ApiParseMarkdown -> "apiParseMarkdown" + is ApiSetContactAlias -> "apiSetContactAlias" is CreateMyAddress -> "createMyAddress" is DeleteMyAddress -> "deleteMyAddress" is ShowMyAddress -> "showMyAddress" @@ -1412,7 +1435,7 @@ sealed class CR { @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() @Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() - @Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats?): CR() @Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation: CR() @@ -1422,6 +1445,7 @@ sealed class CR { @Serializable @SerialName("chatCleared") class ChatCleared(val chatInfo: ChatInfo): CR() @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR() @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() + @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() @Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR() @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR() @@ -1508,6 +1532,7 @@ sealed class CR { is ChatCleared -> "chatCleared" is UserProfileNoChange -> "userProfileNoChange" is UserProfileUpdated -> "userProfileUpdated" + is ContactAliasUpdated -> "contactAliasUpdated" is ParsedMarkdown -> "apiParsedMarkdown" is UserContactLink -> "userContactLink" is UserContactLinkCreated -> "userContactLinkCreated" @@ -1592,6 +1617,7 @@ sealed class CR { is ChatCleared -> json.encodeToString(chatInfo) is UserProfileNoChange -> noDetails() is UserProfileUpdated -> json.encodeToString(toProfile) + is ContactAliasUpdated -> json.encodeToString(toContact) is ParsedMarkdown -> json.encodeToString(formattedText) is UserContactLink -> connReqContact is UserContactLinkCreated -> connReqContact diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index fde270bb38..5b3e0b0ea6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -7,6 +7,7 @@ val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) val Gray = Color(0x22222222) +val Indigo = Color(0xff330099) val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files val SimplexGreen = Color(77, 218, 103, 255) val SecretColor = Color(0x40808080) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 1734a3f1b5..130fbaf864 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -22,7 +23,9 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,15 +36,29 @@ import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @Composable -fun ChatInfoView(chatModel: ChatModel, connStats: ConnectionStats?, close: () -> Unit) { +fun ChatInfoView( + chatModel: ChatModel, + contact: Contact, + connStats: ConnectionStats?, + customUserProfile: Profile?, + localAlias: String, + close: () -> Unit, + onChatUpdated: (Chat) -> Unit, +) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null) { ChatInfoLayout( chat, + contact, connStats, + customUserProfile, + localAlias, developerTools, + onLocalAliasChanged = { + setContactAlias(chat.chatInfo.apiId, it, chatModel, onChatUpdated) + }, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, changeNtfsState = { enabled -> @@ -120,8 +137,12 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit @Composable fun ChatInfoLayout( chat: Chat, + contact: Contact, connStats: ConnectionStats?, + customUserProfile: Profile?, + localAlias: String, developerTools: Boolean, + onLocalAliasChanged: (String) -> Unit, deleteContact: () -> Unit, clearChat: () -> Unit, changeNtfsState: (Boolean) -> Unit, @@ -136,8 +157,18 @@ fun ChatInfoLayout( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - ChatInfoHeader(chat.chatInfo) + ChatInfoHeader(chat.chatInfo, contact) } + + LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged) + + if (customUserProfile != null) { + SectionSpacer() + SectionView(generalGetString(R.string.incognito).uppercase()) { + InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName) + } + } + SectionSpacer() if (connStats != null) { @@ -193,19 +224,20 @@ fun ChatInfoLayout( } @Composable -fun ChatInfoHeader(cInfo: ChatInfo) { +fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { Column( Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) Text( - cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(bottom = 8.dp) ) - if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { + if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) { Text( cInfo.fullName, style = MaterialTheme.typography.h2, color = MaterialTheme.colors.onBackground, @@ -216,6 +248,31 @@ fun ChatInfoHeader(cInfo: ChatInfo) { } } +@Composable +private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { + var value by remember { mutableStateOf(initialValue) } + DefaultBasicTextField( + Modifier.fillMaxWidth().padding(horizontal = 10.dp), + initialValue, + { + Text( + generalGetString(R.string.text_field_set_contact_placeholder), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = HighOrLowlight + ) + }, + color = HighOrLowlight, + textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center), + keyboardActions = KeyboardActions(onDone = { updateValue(value) }) + ) { + value = it + } + DisposableEffect(Unit) { + onDispose { updateValue(value) } + } +} + @Composable fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) { Row( @@ -348,6 +405,13 @@ fun DeleteContactButton(deleteContact: () -> Unit) { } } +private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel, onChatUpdated: (Chat) -> Unit) = withApi { + chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let { + chatModel.updateContact(it) + onChatUpdated(chatModel.getChat(chatModel.chatId.value ?: return@withApi) ?: return@withApi) + } +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -358,9 +422,13 @@ fun PreviewChatInfoLayout() { chatItems = arrayListOf(), serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) ), + Contact.sampleData, + localAlias = "", changeNtfsState = {}, developerTools = false, connStats = null, + onLocalAliasChanged = {}, + customUserProfile = null, deleteContact = {}, clearChat = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index f56e5e90f5..26b0e86636 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -105,18 +105,21 @@ fun ChatView(chatModel: ChatModel) { chatModel.chatItems, searchText, useLinkPreviews = useLinkPreviews, + chatModelIncognito = chatModel.incognito.value, back = { chatModel.chatId.value = null }, info = { withApi { val cInfo = chat.chatInfo if (cInfo is ChatInfo.Direct) { - val connStats = chatModel.controller.apiContactInfo(cInfo.apiId) + val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId) ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - ChatInfoView(chatModel, connStats, close) + ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) { + activeChat = it + } } } } else if (cInfo is ChatInfo.Group) { @@ -191,7 +194,7 @@ fun ChatView(chatModel: ChatModel) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, close) + AddGroupMembersView(groupInfo, chatModel, true, close) } } } @@ -232,6 +235,7 @@ fun ChatLayout( chatItems: List, searchValue: State, useLinkPreviews: Boolean, + chatModelIncognito: Boolean, back: () -> Unit, info: () -> Unit, openDirectChat: (Long) -> Unit, @@ -277,7 +281,7 @@ fun ChatLayout( BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) { ChatItemsList( user, chat, unreadCount, composeState, chatItems, searchValue, - useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage, + useLinkPreviews, chatModelIncognito, openDirectChat, loadPrevMessages, deleteMessage, receiveFile, joinGroup, acceptCall, markRead, setFloatingButton ) } @@ -331,7 +335,7 @@ fun ChatInfoToolbar( startCall(CallMediaType.Video) }) } - } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { + } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) { barButtons.add { IconButton({ showMenu = false @@ -375,6 +379,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { + if (cInfo.incognito) { + IncognitoImage(size = 36.dp, Indigo) + } ChatInfoImage(cInfo, size = imageSize, iconColor) Column( Modifier.padding(start = 8.dp), @@ -384,7 +391,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo cInfo.displayName, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) - if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { + if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.localAlias.isEmpty()) { Text( cInfo.fullName, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -415,6 +422,7 @@ fun BoxWithConstraintsScope.ChatItemsList( chatItems: List, searchValue: State, useLinkPreviews: Boolean, + chatModelIncognito: Boolean, openDirectChat: (Long) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -514,11 +522,11 @@ fun BoxWithConstraintsScope.ChatItemsList( } else { Spacer(Modifier.size(42.dp)) } - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) } } else { Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) { - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall) } } } else { // direct message @@ -529,7 +537,7 @@ fun BoxWithConstraintsScope.ChatItemsList( end = if (sent) 12.dp else 76.dp, ).then(swipeableModifier) ) { - ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall) } } @@ -815,6 +823,7 @@ fun PreviewChatLayout() { chatItems = chatItems, searchValue, useLinkPreviews = true, + chatModelIncognito = false, back = {}, info = {}, openDirectChat = {}, @@ -871,6 +880,7 @@ fun PreviewGroupChatLayout() { chatItems = chatItems, searchValue, useLinkPreviews = true, + chatModelIncognito = false, back = {}, info = {}, openDirectChat = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index 6730da6fb3..28cbd1c48d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -11,11 +11,14 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.TheaterComedy import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -28,7 +31,7 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle import chat.simplex.app.views.helpers.* @Composable -fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { +fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCounter: Boolean = true, close: () -> Unit) { val selectedContacts = remember { mutableStateListOf() } val selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) } @@ -38,6 +41,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> contactsToAdd = getContactsToAdd(chatModel), selectedContacts = selectedContacts, selectedRole = selectedRole, + showFooterCounter = showFooterCounter, inviteMembers = { withApi { selectedContacts.forEach { @@ -75,6 +79,7 @@ fun AddGroupMembersLayout( contactsToAdd: List, selectedContacts: SnapshotStateList, selectedRole: MutableState, + showFooterCounter: Boolean, inviteMembers: () -> Unit, clearSelection: () -> Unit, addContact: (Long) -> Unit, @@ -119,13 +124,15 @@ fun AddGroupMembersLayout( InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty()) } } - SectionCustomFooter { - InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection) + if (showFooterCounter) { + SectionCustomFooter { + InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection) + } } SectionSpacer() SectionView { - ContactList(contacts = contactsToAdd, selectedContacts, addContact, removeContact) + ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, addContact, removeContact) } SectionSpacer() } @@ -255,6 +262,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit) fun ContactList( contacts: List, selectedContacts: SnapshotStateList, + groupInfo: GroupInfo, addContact: (Long) -> Unit, removeContact: (Long) -> Unit ) { @@ -262,7 +270,7 @@ fun ContactList( contacts.forEachIndexed { index, contact -> SectionItemView { ContactCheckRow( - contact, addContact, removeContact, + contact, groupInfo, addContact, removeContact, checked = selectedContacts.contains(contact.apiId) ) } @@ -276,14 +284,35 @@ fun ContactList( @Composable fun ContactCheckRow( contact: Contact, + groupInfo: GroupInfo, addContact: (Long) -> Unit, removeContact: (Long) -> Unit, checked: Boolean ) { + val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito + val icon: ImageVector + val iconColor: Color + if (prohibitedToInviteIncognito) { + icon = Icons.Filled.TheaterComedy + iconColor = HighOrLowlight + } else if (checked) { + icon = Icons.Filled.CheckCircle + iconColor = MaterialTheme.colors.primary + } else { + icon = Icons.Outlined.Circle + iconColor = HighOrLowlight + } Row( Modifier .fillMaxSize() - .clickable { if (!checked) addContact(contact.apiId) else removeContact(contact.apiId) }, + .clickable { + if (prohibitedToInviteIncognito) { + showProhibitedToInviteIncognitoAlertDialog() + } else if (!checked) + addContact(contact.apiId) + else + removeContact(contact.apiId) + }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -292,16 +321,27 @@ fun ContactCheckRow( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { ProfileImage(size = 36.dp, contact.image) - Text(contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (prohibitedToInviteIncognito) HighOrLowlight else Color.Unspecified + ) } Icon( - if (checked) Icons.Filled.CheckCircle else Icons.Outlined.Circle, + icon, contentDescription = stringResource(R.string.icon_descr_contact_checked), - tint = if (checked) MaterialTheme.colors.primary else HighOrLowlight + tint = iconColor ) } } +fun showProhibitedToInviteIncognitoAlertDialog() { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.invite_prohibited), + text = generalGetString(R.string.invite_prohibited_description), + confirmText = generalGetString(R.string.ok), + ) +} + @Preview @Composable fun PreviewAddGroupMembersLayout() { @@ -311,6 +351,7 @@ fun PreviewAddGroupMembersLayout() { contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData), selectedContacts = remember { mutableStateListOf() }, selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }, + showFooterCounter = true, inviteMembers = {}, clearSelection = {}, addContact = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 3c827752c5..0d1208f191 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +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 @@ -24,6 +25,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.* +import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert import chat.simplex.app.views.chatlist.setGroupMembers import chat.simplex.app.views.helpers.* @@ -49,20 +51,20 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, close) + AddGroupMembersView(groupInfo, chatModel, true, close) } } } }, showMemberInfo = { member -> withApi { - val connStats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) + val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) ModalManager.shared.showCustomModal { close -> ModalView( close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - GroupMemberInfoView(groupInfo, member, connStats, chatModel, close) + GroupMemberInfoView(groupInfo, member, stats, chatModel, close) } } } @@ -137,14 +139,16 @@ fun GroupChatInfoLayout( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - ChatInfoHeader(chat.chatInfo) + GroupChatInfoHeader(chat.chatInfo) } SectionSpacer() SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) { if (groupInfo.canAddMembers) { SectionItemView { - AddMembersButton(addMembers) + val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary + val onClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers + AddMembersButton(tint, onClick) } SectionDivider() } @@ -206,7 +210,31 @@ fun GroupChatInfoLayout( } @Composable -fun AddMembersButton(addMembers: () -> Unit) { +fun GroupChatInfoHeader(cInfo: ChatInfo) { + Column( + Modifier.padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + Text( + cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + color = MaterialTheme.colors.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { + Text( + cInfo.fullName, style = MaterialTheme.typography.h2, + color = MaterialTheme.colors.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, addMembers: () -> Unit) { Row( Modifier .fillMaxSize() @@ -216,10 +244,10 @@ fun AddMembersButton(addMembers: () -> Unit) { Icon( Icons.Outlined.Add, stringResource(R.string.button_add_members), - tint = MaterialTheme.colors.primary + tint = tint ) Spacer(Modifier.size(8.dp)) - Text(stringResource(R.string.button_add_members), color = MaterialTheme.colors.primary) + Text(stringResource(R.string.button_add_members), color = tint) } } @@ -251,7 +279,8 @@ fun MemberRow(member: GroupMember, showMemberInfo: ((GroupMember) -> Unit)? = nu ) { ProfileImage(size = 46.dp, member.image) Column { - Text(member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (member.memberIncognito) Indigo else Color.Unspecified) val s = member.memberStatus.shortText val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s Text( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index ba0a39bd3f..5aa06861d7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -27,7 +27,13 @@ import chat.simplex.app.views.chat.SimplexServers import chat.simplex.app.views.helpers.* @Composable -fun GroupMemberInfoView(groupInfo: GroupInfo, member: GroupMember, connStats: ConnectionStats?, chatModel: ChatModel, close: () -> Unit) { +fun GroupMemberInfoView( + groupInfo: GroupInfo, + member: GroupMember, + connStats: ConnectionStats?, + chatModel: ChatModel, + close: () -> Unit +) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val developerTools = chatModel.controller.appPrefs.developerTools.get() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt index 73e7608d2b..3bdd453b41 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R -import chat.simplex.app.TAG import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @@ -28,6 +27,7 @@ fun CIGroupInvitationView( ci: ChatItem, groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole, + chatIncognito: Boolean = false, joinGroup: (Long) -> Unit ) { val sent = ci.chatDir.sent @@ -37,7 +37,7 @@ fun CIGroupInvitationView( fun groupInfoView() { val p = groupInvitation.groupProfile val iconColor = - if (action) MaterialTheme.colors.primary + if (action) if (chatIncognito) Indigo else MaterialTheme.colors.primary else if (isInDarkTheme()) FileDark else FileLight Row( @@ -46,7 +46,7 @@ fun CIGroupInvitationView( .padding(vertical = 4.dp) .padding(end = 2.dp) ) { - ProfileImage(size = 60.dp, icon = Icons.Filled.SupervisedUserCircle, color = iconColor) + ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = Icons.Filled.SupervisedUserCircle, color = iconColor) Spacer(Modifier.padding(horizontal = 3.dp)) Column( Modifier.defaultMinSize(minHeight = 60.dp), @@ -71,13 +71,10 @@ fun CIGroupInvitationView( } } - fun acceptInvitation() { - Log.d(TAG, "CIGroupInvitationView acceptInvitation") - joinGroup(groupInvitation.groupId) - } - Surface( - modifier = if (action) Modifier.clickable(onClick = ::acceptInvitation) else Modifier, + modifier = if (action) Modifier.clickable(onClick = { + joinGroup(groupInvitation.groupId) + }) else Modifier, shape = RoundedCornerShape(18.dp), color = if (sent) SentColorLight else ReceivedColorLight, ) { @@ -99,7 +96,9 @@ fun CIGroupInvitationView( Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) if (action) { groupInvitationText() - Text(stringResource(R.string.group_invitation_tap_to_join), color = MaterialTheme.colors.primary) + Text(stringResource( + if (chatIncognito) R.string.group_invitation_tap_to_join_incognito else R.string.group_invitation_tap_to_join), + color = if (chatIncognito) Indigo else MaterialTheme.colors.primary) } else { Box(Modifier.padding(end = 48.dp)) { groupInvitationText() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 9c8f05238a..a8a806ecbf 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -35,6 +35,7 @@ fun ChatItemView( cxt: Context, uriHandler: UriHandler? = null, showMember: Boolean = false, + chatModelIncognito: Boolean, useLinkPreviews: Boolean, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, @@ -147,8 +148,8 @@ fun ChatItemView( is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember) - is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup) - is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup) + is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) + is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.RcvGroupEventContent -> CIGroupEventView(cItem) is CIContent.SndGroupEventContent -> CIGroupEventView(cItem) } @@ -213,6 +214,7 @@ fun PreviewChatItemView() { useLinkPreviews = true, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, cxt = LocalContext.current, + chatModelIncognito = false, deleteMessage = { _, _ -> }, receiveFile = {}, joinGroup = {}, @@ -232,6 +234,7 @@ fun PreviewChatItemViewDeletedContent() { useLinkPreviews = true, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, cxt = LocalContext.current, + chatModelIncognito = false, deleteMessage = { _, _ -> }, receiveFile = {}, joinGroup = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index fb9faf5457..b8f4d8b1e4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TheaterComedy import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -15,8 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.ui.theme.WarningOrange +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.* import chat.simplex.app.views.chat.group.deleteGroupDialog import chat.simplex.app.views.chat.group.leaveGroupDialog @@ -38,7 +38,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { when (chat.chatInfo) { is ChatInfo.Direct -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, stopped) }, + chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) }, click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, @@ -46,7 +46,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ) is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, stopped) }, + chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) }, click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, showMenu, @@ -54,7 +54,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( - chatLinkPreview = { ContactRequestView(chat.chatInfo) }, + chatLinkPreview = { ContactRequestView(chatModel.incognito.value, chat.chatInfo) }, click = { contactRequestAlertDialog(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) }, showMenu, @@ -128,7 +128,7 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> { - JoinGroupAction(groupInfo, chatModel, showMenu) + JoinGroupAction(chat, groupInfo, chatModel, showMenu) if (groupInfo.canDelete) { DeleteGroupAction(chat, chatModel, showMenu) } @@ -214,12 +214,14 @@ fun DeleteGroupAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { +fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState) { + val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } } ItemAction( - stringResource(R.string.join_group_button), - Icons.Outlined.Login, + if (chat.chatInfo.incognito) stringResource(R.string.join_group_incognito_button) else stringResource(R.string.join_group_button), + if (chat.chatInfo.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.Login, + color = if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.onBackground, onClick = { - withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } + joinGroup() showMenu.value = false } ) @@ -241,8 +243,9 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab @Composable fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState) { ItemAction( - stringResource(R.string.accept_contact_button), - Icons.Outlined.Check, + if (chatModel.incognito.value) stringResource(R.string.accept_contact_incognito_button) else stringResource(R.string.accept_contact_button), + if (chatModel.incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.Check, + color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground, onClick = { acceptContactRequest(chatInfo, chatModel) showMenu.value = false @@ -291,7 +294,7 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel AlertManager.shared.showAlertDialog( title = generalGetString(R.string.accept_connection_request__question), text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified), - confirmText = generalGetString(R.string.accept_contact_button), + confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button), onConfirm = { acceptContactRequest(contactRequest, chatModel) }, dismissText = generalGetString(R.string.reject_contact_button), onDismiss = { rejectContactRequest(contactRequest, chatModel) } @@ -388,13 +391,21 @@ fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) AlertManager.shared.showAlertDialog( title = generalGetString(R.string.join_group_question), text = generalGetString(R.string.you_are_invited_to_group_join_to_connect_with_group_members), - confirmText = generalGetString(R.string.join_group_button), + confirmText = if (groupInfo.membership.memberIncognito) generalGetString(R.string.join_group_incognito_button) else generalGetString(R.string.join_group_button), onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }, dismissText = generalGetString(R.string.delete_verb), onDismiss = { deleteGroup(groupInfo, chatModel) } ) } +fun cantInviteIncognitoAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.alert_title_cant_invite_contacts), + text = generalGetString(R.string.alert_title_cant_invite_contacts_descr), + confirmText = generalGetString(R.string.ok), + ) +} + fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) { withApi { val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId) @@ -473,6 +484,8 @@ fun PreviewChatListNavLinkDirect() { ), chatStats = Chat.ChatStats() ), + false, + null, stopped = false ) }, @@ -508,6 +521,8 @@ fun PreviewChatListNavLinkGroup() { ), chatStats = Chat.ChatStats() ), + false, + null, stopped = false ) }, @@ -530,7 +545,7 @@ fun PreviewChatListNavLinkContactRequest() { SimpleXTheme { ChatListNavLinkLayout( chatLinkPreview = { - ContactRequestView(ChatInfo.ContactRequest.sampleData) + ContactRequestView(false, ChatInfo.ContactRequest.sampleData) }, click = {}, dropdownMenuItems = null, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index a6f1029f34..1698cd2ca6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -8,8 +8,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Report +import androidx.compose.material.icons.filled.TheaterComedy import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource @@ -17,6 +19,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.Indigo import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.NewChatSheet import chat.simplex.app.views.onboarding.MakeConnection @@ -68,7 +71,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() } BottomSheetScaffold( - topBar = { ChatListToolbar(scaffoldCtrl, stopped) }, + topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) }, scaffoldState = scaffoldCtrl.state, drawerContent = { SettingsView(chatModel, setPerformLA) }, sheetPeekHeight = 0.dp, @@ -100,15 +103,25 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: } @Composable -fun ChatListToolbar(scaffoldCtrl: ScaffoldController, stopped: Boolean) { +fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean) { DefaultTopAppBar( navigationButton = { NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } }, title = { - Text( - stringResource(R.string.your_chats), - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.SemiBold, - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(R.string.your_chats), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + if (chatModel.incognito.value) { + Icon( + Icons.Filled.TheaterComedy, + stringResource(R.string.incognito), + tint = Indigo, + modifier = Modifier.padding(10.dp).size(26.dp) + ) + } + } }, onTitleClick = null, showSearch = false, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index dc71336cef..accbffb2e1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -26,7 +26,7 @@ import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.app.views.helpers.* @Composable -fun ChatPreviewView(chat: Chat, stopped: Boolean) { +fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean) { val cInfo = chat.chatInfo @Composable @@ -70,7 +70,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight) is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> chatPreviewTitleText(MaterialTheme.colors.primary) + GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary) GroupMemberStatus.MemAccepted -> chatPreviewTitleText(HighOrLowlight) else -> chatPreviewTitleText() } @@ -79,7 +79,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } @Composable - fun chatPreviewText() { + fun chatPreviewText(chatModelIncognito: Boolean) { val ci = chat.chatItems.lastOrNull() if (ci != null) { MarkdownText( @@ -97,7 +97,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> Text(stringResource(R.string.group_preview_you_are_invited)) + GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = HighOrLowlight) else -> {} } @@ -119,7 +119,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { .weight(1F) ) { chatPreviewTitle() - chatPreviewText() + chatPreviewText(chatModelIncognito) } val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt) @@ -178,6 +178,16 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { } } +@Composable +private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String { + return if (groupInfo.membership.memberIncognito) + String.format(stringResource(R.string.group_preview_join_as), groupInfo.membership.memberProfile.displayName) + else if (chatModelIncognito) + String.format(stringResource(R.string.group_preview_join_as), currentUserProfileDisplayName ?: "") + else + stringResource(R.string.group_preview_you_are_invited) +} + @Composable fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation) @@ -215,6 +225,6 @@ fun ChatStatusImage(chat: Chat) { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, stopped = false) + ChatPreviewView(Chat.sampleData, false, "", stopped = false) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt index 6ec6263f26..70b91e634e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt @@ -10,13 +10,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import chat.simplex.app.R -import chat.simplex.app.model.ChatInfo -import chat.simplex.app.model.getTimestampText +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.ChatInfoImage @Composable -fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { +fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.ContactRequest) { Row { ChatInfoImage(contactRequest, size = 72.dp) Column( @@ -30,7 +29,7 @@ fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, - color = MaterialTheme.colors.primary + color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary ) Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index f4a6595798..5ae9e32341 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -5,6 +5,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import chat.simplex.app.R import chat.simplex.app.TAG @@ -45,7 +46,8 @@ class AlertManager { confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null, dismissText: String = generalGetString(R.string.cancel_verb), - onDismiss: (() -> Unit)? = null + onDismiss: (() -> Unit)? = null, + destructive: Boolean = false ) { val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) } showAlert { @@ -57,7 +59,7 @@ class AlertManager { TextButton(onClick = { onConfirm?.invoke() hideAlert() - }) { Text(confirmText) } + }) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } }, dismissButton = { TextButton(onClick = { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt index 5a596cd5ec..f1d6fa9caa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt @@ -6,8 +6,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.SupervisedUserCircle +import androidx.compose.material.icons.filled.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,6 +30,17 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme ProfileImage(size, chatInfo.image, icon, iconColor) } +@Composable +fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary) { + Box(Modifier.size(size)) { + Icon( + Icons.Filled.TheaterComedy, stringResource(R.string.incognito), + modifier = Modifier.size(size).padding(size / 12), + iconColor + ) + } +} + @Composable fun ProfileImage( size: Dp, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt new file mode 100644 index 0000000000..31de2f5212 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt @@ -0,0 +1,110 @@ +package chat.simplex.app.views.helpers + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.foundation.text.* +import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.indicatorLine +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun DefaultBasicTextField( + modifier: Modifier, + initialValue: String, + placeholder: (@Composable () -> Unit)? = null, + focus: Boolean = false, + color: Color = MaterialTheme.colors.onBackground, + textStyle: TextStyle = TextStyle.Default, + selectTextOnFocus: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions: KeyboardActions = KeyboardActions(), + onValueChange: (String) -> Unit, +) { + val state = remember { + mutableStateOf(TextFieldValue(initialValue)) + } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + if (!focus) return@LaunchedEffect + focusRequester.requestFocus() + delay(200) + keyboard?.show() + } + val enabled = true + val colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Unspecified, + textColor = MaterialTheme.colors.onBackground, + focusedIndicatorColor = Color.Unspecified, + unfocusedIndicatorColor = Color.Unspecified, + ) + val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) + val interactionSource = remember { MutableInteractionSource() } + BasicTextField( + value = state.value, + modifier = modifier + .background(colors.backgroundColor(enabled).value, shape) + .indicatorLine(enabled, false, interactionSource, colors) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + if (focusState.isFocused && selectTextOnFocus) { + val text = state.value.text + state.value = state.value.copy( + selection = TextRange(0, text.length) + ) + } + } + .defaultMinSize( + minWidth = TextFieldDefaults.MinWidth, + minHeight = TextFieldDefaults.MinHeight + ), + onValueChange = { + state.value = it + onValueChange(it.text) + }, + cursorBrush = SolidColor(colors.cursorColor(false).value), + visualTransformation = VisualTransformation.None, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions(onDone = { + keyboard?.hide() + keyboardActions.onDone?.invoke(this) + }), + singleLine = true, + textStyle = textStyle.copy( + color = color, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ), + interactionSource = interactionSource, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = state.value.text, + innerTextField = innerTextField, + placeholder = placeholder, + singleLine = true, + enabled = enabled, + interactionSource = interactionSource, + contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = colors + ) + } + ) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index 0340693610..b2a5bb8bbd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -2,9 +2,10 @@ package chat.simplex.app.views.newchat import android.content.res.Configuration import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TheaterComedy +import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Share import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,8 +19,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.SimpleButton -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.shareText @Composable @@ -28,6 +29,7 @@ fun AddContactView(chatModel: ChatModel) { if (connReq != null) { val cxt = LocalContext.current AddContactLayout( + chatModelIncognito = chatModel.incognito.value, connReq = connReq, share = { shareText(cxt, connReq) } ) @@ -35,22 +37,32 @@ fun AddContactView(chatModel: ChatModel) { } @Composable -fun AddContactLayout(connReq: String, share: () -> Unit) { +fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () -> Unit) { BoxWithConstraints { val screenHeight = maxHeight Column( - horizontalAlignment = Alignment.CenterHorizontally, + Modifier.padding(bottom = 16.dp), verticalArrangement = Arrangement.SpaceBetween, ) { Text( stringResource(R.string.add_contact), style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + modifier = Modifier + .padding(vertical = 5.dp) + .padding(horizontal = 8.dp) ) Text( stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline), - style = MaterialTheme.typography.h3, - textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 8.dp) ) + Row(Modifier.padding(horizontal = 8.dp)) { + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_description), + generalGetString(R.string.your_profile_will_be_sent) + ) + } QRCode( connReq, Modifier .weight(1f, fill = false) @@ -59,14 +71,52 @@ fun AddContactLayout(connReq: String, share: () -> Unit) { ) Text( stringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel), - textAlign = TextAlign.Center, lineHeight = 22.sp, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(horizontal = 8.dp) .padding(bottom = if (screenHeight > 600.dp) 16.dp else 8.dp) ) - SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share) - Spacer(Modifier.height(10.dp)) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share) + } + } + } +} + +@Composable +fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) { + if (chatModelIncognito) { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info, + stringResource(R.string.incognito), + tint = if (supportedIncognito) Indigo else WarningOrange, + modifier = Modifier.padding(end = 10.dp).size(20.dp) + ) + Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2) + } + } else { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Outlined.Info, + stringResource(R.string.incognito), + tint = HighOrLowlight, + modifier = Modifier.padding(end = 10.dp).size(20.dp) + ) + Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2) } } } @@ -81,6 +131,7 @@ fun AddContactLayout(connReq: String, share: () -> Unit) { fun PreviewAddContactView() { SimpleXTheme { AddContactLayout( + chatModelIncognito = false, connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", share = {} ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt index d4d03bfbc3..3f1ce2ef7f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -34,6 +35,7 @@ import kotlinx.coroutines.launch @Composable fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { AddGroupLayout( + chatModel.incognito.value, createGroup = { groupProfile -> withApi { val groupInfo = chatModel.controller.apiNewGroup(groupProfile) @@ -48,7 +50,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, close) + AddGroupMembersView(groupInfo, chatModel, false, close) } } } @@ -59,7 +61,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { } @Composable -fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { +fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> Unit, close: () -> Unit) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val displayName = remember { mutableStateOf("") } @@ -92,11 +94,16 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { ) { Text( stringResource(R.string.create_secret_group_title), - style = MaterialTheme.typography.h4, + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), modifier = Modifier.padding(vertical = 5.dp) ) - ReadableText(R.string.group_is_decentralized) - Spacer(Modifier.height(10.dp)) + Text(stringResource(R.string.group_is_decentralized)) + InfoAboutIncognito( + chatModelIncognito, + false, + generalGetString(R.string.group_unsupported_incognito_main_profile_sent), + generalGetString(R.string.group_main_profile_sent) + ) Box( Modifier .fillMaxWidth() @@ -170,6 +177,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) { fun PreviewAddGroupLayout() { SimpleXTheme { AddGroupLayout( + chatModelIncognito = false, createGroup = {}, close = {} ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt index 6e9372dcc2..bfb62fd89b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt @@ -31,6 +31,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { val clipboard = getSystemService(context, ClipboardManager::class.java) BackHandler(onBack = close) PasteToConnectLayout( + chatModel.incognito.value, connectionLink = connectionLink, pasteFromClipboard = { connectionLink.value = clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context) as String @@ -55,6 +56,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { @Composable fun PasteToConnectLayout( + chatModelIncognito: Boolean, connectionLink: MutableState, pasteFromClipboard: () -> Unit, connectViaLink: (String) -> Unit, @@ -62,16 +64,22 @@ fun PasteToConnectLayout( ) { ModalView(close) { Column( - horizontalAlignment = Alignment.CenterHorizontally, + Modifier.padding(bottom = 16.dp), verticalArrangement = Arrangement.SpaceBetween, ) { Text( stringResource(R.string.connect_via_link), style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(vertical = 5.dp) ) Text(stringResource(R.string.paste_connection_link_below_to_connect)) - Text(stringResource(R.string.profile_will_be_sent_to_contact_sending_link)) + + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_from_contact_description), + generalGetString(R.string.profile_will_be_sent_to_contact_sending_link) + ) Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) { TextEditor(Modifier.height(180.dp), text = connectionLink) @@ -111,6 +119,7 @@ fun PasteToConnectLayout( fun PreviewPasteToConnectTextbox() { SimpleXTheme { PasteToConnectLayout( + chatModelIncognito = false, connectionLink = remember { mutableStateOf("") }, pasteFromClipboard = {}, connectViaLink = { link -> diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt index 11c67a409a..465591c544 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -22,6 +21,7 @@ import chat.simplex.app.views.helpers.* fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { BackHandler(onBack = close) ConnectContactLayout( + chatModelIncognito = chatModel.incognito.value, qrCodeScanner = { QRCodeScanner { connReqUri -> try { @@ -67,21 +67,22 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) { } @Composable -fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { +fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { ModalView(close) { Column( - horizontalAlignment = Alignment.CenterHorizontally, + Modifier.padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( generalGetString(R.string.scan_QR_code), style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + modifier = Modifier.padding(vertical = 5.dp) ) - Text( - generalGetString(R.string.your_chat_profile_will_be_sent_to_your_contact), - style = MaterialTheme.typography.h3, - textAlign = TextAlign.Center, - modifier = Modifier.padding(bottom = 4.dp) + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_description), + generalGetString(R.string.your_profile_will_be_sent) ) Box( Modifier @@ -106,6 +107,7 @@ fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Uni fun PreviewConnectContactLayout() { SimpleXTheme { ConnectContactLayout( + chatModelIncognito = false, qrCodeScanner = { Surface {} }, close = {}, ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt index 95478fd9d9..7535138ee7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt @@ -3,11 +3,15 @@ package chat.simplex.app.views.usersettings import SectionDivider import SectionItemView import SectionView +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import chat.simplex.app.R @@ -82,6 +86,40 @@ fun SharedPreferenceToggle( } } +@Composable +fun SharedPreferenceToggleWithIcon( + text: String, + icon: ImageVector, + stopped: Boolean = false, + onClickInfo: () -> Unit, + preference: Preference, + preferenceState: MutableState? = null +) { + val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) } + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text(text, Modifier.padding(end = 4.dp)) + Icon( + icon, + null, + Modifier.clickable(onClick = onClickInfo), + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Switch( + checked = prefState.value, + onCheckedChange = { + preference.set(it) + prefState.value = it + }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + enabled = !stopped + ) + } +} + @Composable fun SharedPreferenceRadioButton(text: String, prefState: MutableState, preference: Preference, value: T) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt new file mode 100644 index 0000000000..002dedf43a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt @@ -0,0 +1,47 @@ +package chat.simplex.app.views.usersettings + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.views.helpers.generalGetString + +@Composable +fun IncognitoView() { + IncognitoLayout() +} + +@Composable +fun IncognitoLayout() { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Text( + stringResource(R.string.settings_section_title_incognito), + Modifier.padding(start = 8.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 8.dp) + ) { + Column( + Modifier.padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Text(generalGetString(R.string.incognito_info_protects)) + Text(generalGetString(R.string.incognito_info_allows)) + Text(generalGetString(R.string.incognito_info_share)) + Text(generalGetString(R.string.incognito_info_find)) + } + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 9061369acc..1e0a24beff 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Report +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -37,6 +37,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false + MaintainIncognitoState(chatModel) + fun setRunServiceInBackground(on: Boolean) { chatModel.controller.appPrefs.runServiceInBackground.set(on) if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) { @@ -51,6 +53,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { SettingsLayout( profile = user.profile, stopped, + chatModel.incognito, + chatModel.controller.appPrefs.incognito, runServiceInBackground = chatModel.runServiceInBackground, developerTools = chatModel.controller.appPrefs.developerTools, setRunServiceInBackground = ::setRunServiceInBackground, @@ -84,8 +88,10 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: Profile, + profile: LocalProfile, stopped: Boolean, + incognito: MutableState, + incognitoPref: Preference, runServiceInBackground: MutableState, developerTools: Preference, setRunServiceInBackground: (Boolean) -> Unit, @@ -116,6 +122,8 @@ fun SettingsLayout( ProfilePreview(profile, stopped = stopped) } SectionDivider() + SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { onClickIncognitoInfo(showModal) } + SectionDivider() SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped) SectionDivider() DatabaseItem(showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) @@ -163,6 +171,47 @@ fun SettingsLayout( } } +@Composable +fun SettingsIncognitoActionItem( + incognitoPref: Preference, + incognito: MutableState, + stopped: Boolean, + onClickInfo: () -> Unit, +) { + SettingsPreferenceItemWithInfo( + if (incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy, + if (incognito.value) Indigo else HighOrLowlight, + stringResource(R.string.incognito), + stopped, + onClickInfo, + incognitoPref, + incognito + ) +} + +private val onClickIncognitoInfo: ((@Composable (ChatModel) -> Unit) -> (() -> Unit)) -> Unit = { showModal -> + showModal { IncognitoView() }() +} + +@Composable +fun MaintainIncognitoState(chatModel: ChatModel) { + // Cache previous value and once it changes in background, update it via API + var cachedIncognito by remember { mutableStateOf(chatModel.incognito.value) } + LaunchedEffect(chatModel.incognito.value) { + // Don't do anything if nothing changed + if (cachedIncognito == chatModel.incognito.value) return@LaunchedEffect + try { + chatModel.controller.apiSetIncognito(chatModel.incognito.value) + } catch (e: Exception) { + // Rollback the state + chatModel.controller.appPrefs.incognito.set(cachedIncognito) + // Crash the app + throw e + } + cachedIncognito = chatModel.incognito.value + } +} + @Composable private fun DatabaseItem(openDatabaseView: () -> Unit, stopped: Boolean) { SectionItemView(openDatabaseView) { Row( @@ -322,6 +371,25 @@ fun SettingsPreferenceItem(icon: ImageVector, text: String, pref: Preference Unit, + pref: Preference, + prefState: MutableState? = null +) { + SectionItemView() { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onClickInfo() }) { + Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint) + Spacer(Modifier.padding(horizontal = 4.dp)) + SharedPreferenceToggleWithIcon(text, Icons.Outlined.Info, stopped, onClickInfo, pref, prefState) + } + } +} + @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -332,8 +400,10 @@ fun SettingsPreferenceItem(icon: ImageVector, text: String, pref: Preference Unit) { val user = chatModel.currentUser.value if (user != null) { val editProfile = remember { mutableStateOf(false) } - var profile by remember { mutableStateOf(user.profile) } + var profile by remember { mutableStateOf(user.profile.toProfile()) } UserProfileLayout( close = close, editProfile = editProfile, @@ -47,7 +46,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { val p = Profile(displayName, fullName, image) val newProfile = chatModel.controller.apiUpdateProfile(p) if (newProfile != null) { - chatModel.updateUserProfile(newProfile) + chatModel.currentUser.value?.profile?.profileId?.let { + chatModel.updateUserProfile(newProfile.toLocalProfile(it)) + } profile = newProfile } editProfile.value = false diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 994672296a..023e067f1c 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -31,8 +31,11 @@ приглашение соединиться соединяется… вы создали одноразовую ссылку + вы создали одноразовую ссылку инкогнито через ссылку-контакт + инкогнито через ссылку-контакт через одноразовую ссылку + инкогнито через одноразовую ссылку Ошибка при сохранении SMP серверов @@ -114,6 +117,7 @@ Ваши чаты соединяется… вы приглашены в группу + вступить как %s соединяется… @@ -148,6 +152,7 @@ Удалить контакт? Контакт и все сообщения будут удалены - это действие нельзя отменить! Удалить контакт + Имя контакта… Соединение с сервером установлено Соединение с сервером не установлено Ошибка соединения с сервером @@ -160,7 +165,7 @@ Назад Отменить Подтвердить - + OK нет описания Добавить контакт Скопировано в буфер обмена @@ -199,6 +204,7 @@ Принять запрос на соединение? Отправителю НЕ будет послано уведомление, если вы отклоните запрос на соединение. Принять + Принять инкогнито Отклонить @@ -251,13 +257,15 @@ Запрос на соединение послан! Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже! - Покажите QR код вашему контакту, чтобы сосканировать его из приложения + Покажите QR код вашему контакту, чтобы сосканировать его из приложения. Если вы не можете встретиться лично, вы можете показать QR код во время видеозвонка или отправить ссылку через любой другой канал связи. Ваш профиль будет отправлен\nвашему контакту Если вы не можете встретиться лично, вы можете сосканировать QR код во время видеозвонка, или ваш контакт может отправить вам ссылку. Поделиться ссылкой Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта. - + Ваш профиль будет отправлен вашему контакту + + Настройки Ваш SimpleX адрес @@ -469,6 +477,7 @@ SOCKS ПРОКСИ ИКОНКА ТЕМЫ + Режим Инкогнито Данные чата @@ -519,6 +528,7 @@ Вступить в группу? Вы приглашены в группу. Вступите, чтобы соединиться с членами группы. Вступить + Вступить инкогнито Вступление в группу Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. Выйти @@ -531,11 +541,14 @@ Группа не найдена! Эта группа больше не существует. Ошибка приглашения + Нельзя пригласить контакты! + Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено Вы отправили приглашение в группу Вы приглашены в группу Нажмите, чтобы вступить + Нажмите, чтобы вступить инкогнито Вы вступили в эту группу Вы отклонили приглашение в группу Приглашение в группу истекло @@ -581,6 +594,8 @@ Очистить Выбрано контактов: %1$s Контакты не выбраны + Нельзя пригласить контакт! + Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль Пригласить членов группы @@ -618,6 +633,8 @@ Группа полностью децентрализована — она видна только членам. Имя группы: Полное имя: + Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы + Ваш профиль чата будет отправлен членам группы Профиль группы хранится на устройствах членов, а не на серверах. @@ -637,6 +654,17 @@ Обновление настроек приведет к переподключению клиента ко всем серверам. Обновить + + Инкогнито + Случайный профиль + Вашему контакту будет отправлен случайный профиль + Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль + + Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль. + Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя. + Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. + Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата. + Системная Светлая diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 8019e862ab..cf21338f7a 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -31,8 +31,11 @@ invited to connect connecting… you shared one-time link + you shared one-time link incognito via contact address link + incognito via contact address link via one-time link + incognito via one-time link Error saving SMP servers @@ -114,6 +117,7 @@ Your chats connecting… you are invited to group + join as %s connecting… @@ -148,6 +152,7 @@ Delete contact? Contact and all messages will be deleted - this cannot be undone! Delete contact + Set contact name… Connected Disconnected Error @@ -160,7 +165,7 @@ Back Cancel Confirm - Ok + OK no details Add contact Copied to clipboard @@ -199,6 +204,7 @@ Accept connection request? If you choose to reject sender will NOT be notified. Accept + Accept incognito Reject @@ -251,12 +257,13 @@ Connection request sent! You will be connected when your connection request is accepted, please wait or check later! You will be connected when your contact\'s device is online, please wait or check later! - Show QR code for your contact\nto scan from the app - If you cannot meet in person, you can show QR code in the video call, or you can share the invitation link via any other channel. + Show QR code for your contact to scan from the app. + If you can\'t meet in person, you can show QR code in the video call, or you can share the invitation link via any other channel. Your chat profile will be sent\nto your contact If you cannot meet in person, you can scan QR code in the video call, or your contact can share an invitation link. Share invitation link Paste the link you received into the box below to connect with your contact. + Your chat profile will be sent to your contact Connect via link @@ -471,6 +478,7 @@ SOCKS PROXY APP ICON THEMES + Incognito mode Your chat database @@ -521,6 +529,7 @@ Join group? You are invited to group. Join to connect with group members. Join + Join incognito Joining group You joined this group. Connecting to inviting group member. Leave @@ -533,11 +542,14 @@ Group not found! This group no longer exists. Error joining group + Can\'t invite contacts! + You\'re using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed You sent group invitation You are invited to group Tap to join + Tap to join incognito You joined this group You rejected group invitation Group invitation expired @@ -583,6 +595,8 @@ Clear %1$s contact(s) selected No contacts selected + Can\'t invite contact! + You\'re trying to invite contact with whom you\'ve shared an incognito profile to the group in which you\'re using your main profile Invite members @@ -620,6 +634,9 @@ The group is fully decentralized – it is visible only to the members. Group display name: Group full name: + Incognito mode is not supported here - your main profile will be sent to group members + Your chat profile will be sent to group members + Group profile is stored on members\' devices, not on the servers. @@ -639,6 +656,17 @@ Updating settings will re-connect the client to all servers. Update + + Incognito + Your random profile + A random profile will be sent to your contact + A random profile will be sent to the contact that you received this link from + + Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. + It allows having many anonymous connections without any shared data between them in a single chat profile. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. + To find the profile used for an incognito connection, tap the contact or group name on top of the chat. + System Light From 5fa2d7f7cee7b6465ae2cb9152004c51ed2407a6 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 30 Aug 2022 15:33:36 +0400 Subject: [PATCH 63/89] ios: disable incognito toggle when chat is stopped (#983) --- apps/ios/Shared/Views/UserSettings/SettingsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 053b578d31..c15077c7dd 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -78,6 +78,7 @@ struct SettingsView: View { .disabled(chatModel.chatRunning != true) incognitoRow() + .disabled(chatModel.chatRunning != true) NavigationLink { UserAddress() From 69758971afb27a2d96c5582c982b981d735ff21b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:17:48 +0100 Subject: [PATCH 64/89] update simplexmq (to fix network-transport at 0.5.4) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 27b2d7cb31..78e56eb4fe 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef + tag: f2c1455a2755e1275983dc154321fc0a5c0d7b17 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 76ea520034..21d0d67735 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef" = "0iqk58dhckpij9l1z8bm83hghw5cwj9hmpkbk7j8vws123g1bd73"; + "https://github.com/simplex-chat/simplexmq.git"."f2c1455a2755e1275983dc154321fc0a5c0d7b17" = "10l74d751jmgsr0ifyprglsvqdpcir86qs1vkwc4dn4n4q503p5q"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 4b4865916f..a77d573f0f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef + commit: f2c1455a2755e1275983dc154321fc0a5c0d7b17 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 92abdde69eb385b66bc246ae85f28fd986c5651d Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 30 Aug 2022 20:37:44 +0300 Subject: [PATCH 65/89] android: start direct chat button inside MemberInfo (#987) * Start direct chat button inside GroupInfo * More code for better understanding --- .../java/chat/simplex/app/model/ChatModel.kt | 1 + .../chat/simplex/app/views/chat/ChatView.kt | 34 ++++++------- .../app/views/chat/group/GroupChatInfoView.kt | 6 +-- .../views/chat/group/GroupMemberInfoView.kt | 48 ++++++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 1 + .../app/src/main/res/values/strings.xml | 1 + 6 files changed, 71 insertions(+), 20 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 918a6a30e4..8593d903bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -67,6 +67,7 @@ class ChatModel(val controller: ChatController) { fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } + fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 26b0e86636..c6744f1361 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -32,8 +32,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* -import chat.simplex.app.views.chat.group.AddGroupMembersView -import chat.simplex.app.views.chat.group.GroupChatInfoView +import chat.simplex.app.views.chat.group.* import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.chatlist.* @@ -135,12 +134,17 @@ fun ChatView(chatModel: ChatModel) { } } }, - openDirectChat = { contactId -> - val c = chatModel.chats.firstOrNull { - it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId - } - if (c != null) { - withApi { openChat(c.chatInfo, chatModel) } + showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> + withApi { + val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) + ModalManager.shared.showCustomModal { close -> + ModalView( + close = close, modifier = Modifier, + background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight + ) { + GroupMemberInfoView(groupInfo, member, stats, chatModel, close, close) + } + } } }, loadPrevMessages = { cInfo -> @@ -238,7 +242,7 @@ fun ChatLayout( chatModelIncognito: Boolean, back: () -> Unit, info: () -> Unit, - openDirectChat: (Long) -> Unit, + showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, @@ -281,7 +285,7 @@ fun ChatLayout( BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) { ChatItemsList( user, chat, unreadCount, composeState, chatItems, searchValue, - useLinkPreviews, chatModelIncognito, openDirectChat, loadPrevMessages, deleteMessage, + useLinkPreviews, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage, receiveFile, joinGroup, acceptCall, markRead, setFloatingButton ) } @@ -423,7 +427,7 @@ fun BoxWithConstraintsScope.ChatItemsList( searchValue: State, useLinkPreviews: Boolean, chatModelIncognito: Boolean, - openDirectChat: (Long) -> Unit, + showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, @@ -510,9 +514,7 @@ fun BoxWithConstraintsScope.ChatItemsList( Modifier .clip(CircleShape) .clickable { - openDirectChat(contactId) - // Scroll to first unread message when direct chat will be loaded - shouldAutoScroll = true + showMemberInfo(chat.chatInfo.groupInfo, member) } ) { MemberImage(member) @@ -826,7 +828,7 @@ fun PreviewChatLayout() { chatModelIncognito = false, back = {}, info = {}, - openDirectChat = {}, + showMemberInfo = {_, _ -> }, loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, receiveFile = {}, @@ -883,7 +885,7 @@ fun PreviewGroupChatLayout() { chatModelIncognito = false, back = {}, info = {}, - openDirectChat = {}, + showMemberInfo = {_, _ -> }, loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, receiveFile = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 0d1208f191..84250861da 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -59,12 +59,12 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { showMemberInfo = { member -> withApi { val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) - ModalManager.shared.showCustomModal { close -> + ModalManager.shared.showCustomModal { closeCurrent -> ModalView( - close = close, modifier = Modifier, + close = closeCurrent, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - GroupMemberInfoView(groupInfo, member, stats, chatModel, close) + GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 5aa06861d7..53a19528fe 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -24,6 +24,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.SimplexServers +import chat.simplex.app.views.chatlist.openChat import chat.simplex.app.views.helpers.* @Composable @@ -32,7 +33,8 @@ fun GroupMemberInfoView( member: GroupMember, connStats: ConnectionStats?, chatModel: ChatModel, - close: () -> Unit + close: () -> Unit, + closeAll: () -> Unit, // Close all open windows up to ChatView ) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } @@ -43,6 +45,22 @@ fun GroupMemberInfoView( member, connStats, developerTools, + openDirectChat = { + withApi { + val oldChat = chatModel.getContactChat(member.memberContactId ?: return@withApi) + if (oldChat != null) { + openChat(oldChat.chatInfo, chatModel) + } else { + var newChat = chatModel.controller.apiGetChat(ChatType.Direct, member.memberContactId) ?: return@withApi + // TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend + newChat = newChat.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected())) + chatModel.addChat(newChat) + chatModel.chatItems.clear() + chatModel.chatId.value = newChat.id + } + closeAll() + } + }, removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) } ) } @@ -71,6 +89,7 @@ fun GroupMemberInfoLayout( member: GroupMember, connStats: ConnectionStats?, developerTools: Boolean, + openDirectChat: () -> Unit, removeMember: () -> Unit, ) { Column( @@ -87,6 +106,13 @@ fun GroupMemberInfoLayout( } SectionSpacer() + SectionView { + SectionItemView { + OpenChatButton(openDirectChat) + } + } + SectionSpacer() + SectionView(title = stringResource(R.string.member_info_section_title_member)) { InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName) val conn = member.activeConn @@ -181,6 +207,25 @@ fun RemoveMemberButton(removeMember: () -> Unit) { } } +@Composable +fun OpenChatButton(onClick: () -> Unit) { + Row( + Modifier + .fillMaxSize() + .clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Outlined.Message, + stringResource(R.string.button_send_direct_message), + Modifier.padding(top = 5.dp), + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.button_send_direct_message), color = MaterialTheme.colors.primary) + } +} + @Preview @Composable fun PreviewGroupMemberInfoLayout() { @@ -190,6 +235,7 @@ fun PreviewGroupMemberInfoLayout() { member = GroupMember.sampleData, connStats = null, developerTools = false, + openDirectChat = {}, removeMember = {} ) } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 023e067f1c..960e274fa5 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -614,6 +614,7 @@ Удалить члена группы + Отправить сообщение Член группы будет удален - это действие нельзя отменить! Удалить ЧЛЕН ГРУППЫ diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index cf21338f7a..5ce4e0556f 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -615,6 +615,7 @@ Remove member + Send direct message Member will be removed from group - this cannot be undone! Remove MEMBER From 378118b82e44490c050e83ad0dd6d63ad40f5da7 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:24:33 +0300 Subject: [PATCH 66/89] Options when using .onion hosts (#989) * Options when using .onion hosts * Confirmation alert before applying network settings * Useless new line was removed * Different ordering of options in enum --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 30 ++++ .../views/usersettings/NetworkAndServers.kt | 157 +++++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 4 + .../app/src/main/res/values/strings.xml | 4 + 4 files changed, 188 insertions(+), 7 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 95da5ff799..30da6db484 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -88,6 +88,8 @@ class AppPreferences(val context: Context) { val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) + val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) + val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) @@ -159,6 +161,8 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" + private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" + private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" @@ -1103,6 +1107,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun getNetCfg(): NetCfg { val useSocksProxy = appPrefs.networkUseSocksProxy.get() val socksProxy = if (useSocksProxy) ":9050" else null + val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) + val requiredHostMode = appPrefs.networkRequiredHostMode.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() @@ -1117,6 +1123,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } return NetCfg( socksProxy = socksProxy, + hostMode = hostMode, + requiredHostMode = requiredHostMode, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpKeepAlive = tcpKeepAlive, @@ -1126,6 +1134,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun setNetCfg(cfg: NetCfg) { appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) + appPrefs.networkHostMode.set(cfg.hostMode.name) + appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) @@ -1370,6 +1380,26 @@ data class NetCfg( smpPingInterval = 600_000_000 ) } + + val onionHosts: OnionHosts get() = when { + hostMode == HostMode.Public && requiredHostMode -> OnionHosts.NEVER + hostMode == HostMode.OnionViaSocks && !requiredHostMode -> OnionHosts.PREFER + hostMode == HostMode.OnionViaSocks && requiredHostMode -> OnionHosts.REQUIRED + else -> OnionHosts.PREFER + } + + fun withOnionHosts(mode: OnionHosts): NetCfg = when (mode) { + OnionHosts.NEVER -> + this.copy(hostMode = HostMode.Public, requiredHostMode = true) + OnionHosts.PREFER -> + this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = false) + OnionHosts.REQUIRED -> + this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = true) + } +} + +enum class OnionHosts { + NEVER, PREFER, REQUIRED } @Serializable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt index 0ac31464fb..a6fd331681 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt @@ -10,12 +10,14 @@ import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NetCfg +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @@ -25,16 +27,19 @@ fun NetworkAndServersView( showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) ) { - val netCfg: MutableState = remember { mutableStateOf(chatModel.controller.getNetCfg()) } - val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.value.useSocksProxy) } + // It's not a state, just a one-time value. Shouldn't be used in any state-related situations + val netCfg = remember { chatModel.controller.getNetCfg() } + val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } val developerTools = chatModel.controller.appPrefs.developerTools.get() + val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } NetworkAndServersLayout( developerTools = developerTools, networkUseSocksProxy = networkUseSocksProxy, + onionHosts = onionHosts, showModal = showModal, showSettingsModal = showSettingsModal, - toggleSocksProxy = { enable -> + toggleSocksProxy = { enable -> if (enable) { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.network_enable_socks), @@ -45,6 +50,7 @@ fun NetworkAndServersView( chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults) chatModel.controller.setNetCfg(NetCfg.proxyDefaults) networkUseSocksProxy.value = true + onionHosts.value = NetCfg.proxyDefaults.onionHosts } } ) @@ -58,10 +64,29 @@ fun NetworkAndServersView( chatModel.controller.apiSetNetworkConfig(NetCfg.defaults) chatModel.controller.setNetCfg(NetCfg.defaults) networkUseSocksProxy.value = false + onionHosts.value = NetCfg.defaults.onionHosts } } ) } + }, + useOnion = { + val prevValue = onionHosts.value + onionHosts.value = it + updateNetworkSettingsDialog(onDismiss = { + onionHosts.value = prevValue + }) { + withApi { + val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it) + val res = chatModel.controller.apiSetNetworkConfig(newCfg) + if (res) { + chatModel.controller.setNetCfg(newCfg) + onionHosts.value = it + } else { + onionHosts.value = prevValue + } + } + } } ) } @@ -69,9 +94,11 @@ fun NetworkAndServersView( @Composable fun NetworkAndServersLayout( developerTools: Boolean, networkUseSocksProxy: MutableState, + onionHosts: MutableState, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - toggleSocksProxy: (Boolean) -> Unit + toggleSocksProxy: (Boolean) -> Unit, + useOnion: (OnionHosts) -> Unit, ) { Column( Modifier.fillMaxWidth(), @@ -89,6 +116,10 @@ fun NetworkAndServersView( SectionItemView { UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) } + SectionDivider() + SectionItemView { + UseOnionHosts(onionHosts, networkUseSocksProxy, useOnion) + } if (developerTools) { SectionDivider() SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) @@ -129,6 +160,116 @@ fun UseSocksProxySwitch( } } +@Composable +private fun UseOnionHosts(onionHosts: MutableState, enabled: State, useOnion: (OnionHosts) -> Unit) { + val values = remember { + OnionHosts.values().map { + when (it) { + OnionHosts.NEVER -> OnionHosts.NEVER to generalGetString(R.string.network_use_onion_hosts_no) + OnionHosts.PREFER -> OnionHosts.PREFER to generalGetString(R.string.network_use_onion_hosts_prefer) + OnionHosts.REQUIRED -> OnionHosts.REQUIRED to generalGetString(R.string.network_use_onion_hosts_required) + } + } + } + ExposedDropDownSettingRow( + generalGetString(R.string.network_use_onion_hosts), + values, + onionHosts, + icon = Icons.Outlined.Security, + enabled = enabled, + onSelected = useOnion + ) +} + +@Composable +fun ExposedDropDownSettingRow( + title: String, + values: List>, + selection: State, + label: String? = null, + icon: ImageVector? = null, + iconTint: Color = HighOrLowlight, + enabled: State = mutableStateOf(true), + onSelected: (T) -> Unit +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + var expanded by remember { mutableStateOf(false) } + + if (icon != null) { + Icon( + icon, + "", + Modifier.padding(end = 8.dp), + tint = iconTint + ) + } + Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight) + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded && enabled.value + } + ) { + Row( + Modifier.padding(start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = HighOrLowlight + ) + Spacer(Modifier.size(12.dp)) + Icon( + if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess, + generalGetString(R.string.icon_descr_more_button), + tint = HighOrLowlight + ) + } + ExposedDropdownMenu( + modifier = Modifier.widthIn(min = 200.dp), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded = false + } + ) { + Text( + selectionOption.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +private fun updateNetworkSettingsDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.update_network_settings_question), + text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers), + confirmText = generalGetString(R.string.update_network_settings_confirmation), + onDismiss = onDismiss, + onConfirm = onConfirm, + ) +} + @Preview(showBackground = true) @Composable fun PreviewNetworkAndServersLayout() { @@ -138,7 +279,9 @@ fun PreviewNetworkAndServersLayout() { networkUseSocksProxy = remember { mutableStateOf(true) }, showModal = { {} }, showSettingsModal = { {} }, - toggleSocksProxy = {} + toggleSocksProxy = {}, + onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, + useOnion = {}, ) } } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 960e274fa5..928cf18d0d 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -297,6 +297,10 @@ Соединяться с серверами через SOCKS прокси через порт 9050? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? Если вы подтвердите, серверы смогут видеть ваш IP адрес, а провайдер - с какими серверами вы соединяетесь. + Использовать .onion хосты + Когда возможно + Нет + Обязательно Интерфейс diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 5ce4e0556f..9e3651b5dd 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -301,6 +301,10 @@ Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option. Use direct Internet connection? If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to. + Use .onion hosts + When available + No + Required Appearance From 956cf6b2036fcbc49c3a0c4497019abdeeb0edbb Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 31 Aug 2022 09:45:18 +0400 Subject: [PATCH 67/89] android: version 3.2 (50) --- apps/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 4fc7791a26..83c1a24a47 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 49 + versionCode 50 versionName "3.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 73447ce22beda6658b93ba88f37dff5678ccfd3f Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 31 Aug 2022 09:55:21 +0400 Subject: [PATCH 68/89] android: version 3.2 (51) --- apps/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 83c1a24a47..8d439ee0ce 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 50 + versionCode 51 versionName "3.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From a1562bf0e710f94a8e1c6422aedd6dc85c2fde08 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 31 Aug 2022 12:26:41 +0400 Subject: [PATCH 69/89] android: restore footer counter (#990) --- .../main/java/chat/simplex/app/views/chat/ChatView.kt | 2 +- .../app/views/chat/group/AddGroupMembersView.kt | 11 +++-------- .../simplex/app/views/chat/group/GroupChatInfoView.kt | 2 +- .../chat/simplex/app/views/newchat/AddGroupView.kt | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index c6744f1361..d95afec8ed 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -198,7 +198,7 @@ fun ChatView(chatModel: ChatModel) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, true, close) + AddGroupMembersView(groupInfo, chatModel, close) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index 28cbd1c48d..252543d150 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -31,7 +31,7 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle import chat.simplex.app.views.helpers.* @Composable -fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCounter: Boolean = true, close: () -> Unit) { +fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { val selectedContacts = remember { mutableStateListOf() } val selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) } @@ -41,7 +41,6 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCo contactsToAdd = getContactsToAdd(chatModel), selectedContacts = selectedContacts, selectedRole = selectedRole, - showFooterCounter = showFooterCounter, inviteMembers = { withApi { selectedContacts.forEach { @@ -79,7 +78,6 @@ fun AddGroupMembersLayout( contactsToAdd: List, selectedContacts: SnapshotStateList, selectedRole: MutableState, - showFooterCounter: Boolean, inviteMembers: () -> Unit, clearSelection: () -> Unit, addContact: (Long) -> Unit, @@ -124,10 +122,8 @@ fun AddGroupMembersLayout( InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty()) } } - if (showFooterCounter) { - SectionCustomFooter { - InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection) - } + SectionCustomFooter { + InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection) } SectionSpacer() @@ -351,7 +347,6 @@ fun PreviewAddGroupMembersLayout() { contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData), selectedContacts = remember { mutableStateListOf() }, selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }, - showFooterCounter = true, inviteMembers = {}, clearSelection = {}, addContact = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 84250861da..587b3f40aa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -51,7 +51,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, true, close) + AddGroupMembersView(groupInfo, chatModel, close) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt index 3f1ce2ef7f..f18ccecbcd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt @@ -50,7 +50,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { close = close, modifier = Modifier, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - AddGroupMembersView(groupInfo, chatModel, false, close) + AddGroupMembersView(groupInfo, chatModel, close) } } } From 74b11d1c5dea70887e8492cb9fb769bd2654426a Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:13:40 +0400 Subject: [PATCH 70/89] 3.2.1 (#992) --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 893402be70..5a86bdba40 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 3.2.0 +version: 3.2.1 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 662df81c05..c64ea90a6e 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 3.2.0 +version: 3.2.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 1e587df3d48427aaac79e74e2ee0d6b57877c868 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:49:19 +0300 Subject: [PATCH 71/89] State preserving for some UI elements which otherwise would be lost on orientation change (#994) - restore message text as well as reply state - restore search view - don't display blank view on orientation change for a moment - better saving of local user name while typing. Prevent loosing state on orientation change and hard killing the app - don't display same messages in MainActivity from old intents on orientation change (no double processing of intent) --- .../java/chat/simplex/app/MainActivity.kt | 9 ++++-- .../simplex/app/views/chat/ChatInfoView.kt | 18 +++++++++-- .../chat/simplex/app/views/chat/ChatView.kt | 12 ++++--- .../simplex/app/views/chat/ComposeView.kt | 32 +++++++++++++------ .../app/views/helpers/SearchTextField.kt | 14 ++++---- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 163d09a8af..19dbaec96d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.material.Surface import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Replay import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -51,7 +52,11 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(this) // testJson() val m = vm.chatModel - processNotificationIntent(intent, m) + // When call ended and orientation changes, it re-process old intent, it's unneeded. + // Only needed to be processed on first creation of activity + if (savedInstanceState == null) { + processNotificationIntent(intent, m) + } setContent { SimpleXTheme { Surface( @@ -223,7 +228,7 @@ fun MainPage( showLANotice: () -> Unit ) { // this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication - var chatsAccessAuthorized by remember { mutableStateOf(false) } + var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) } LaunchedEffect(userAuthorized.value) { if (chatModel.controller.appPrefs.performLA.get()) { delay(500L) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 130fbaf864..cd84fb6348 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,6 +35,8 @@ import chat.simplex.app.SimplexApp import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* @Composable fun ChatInfoView( @@ -250,10 +253,10 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { @Composable private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { - var value by remember { mutableStateOf(initialValue) } + var value by rememberSaveable { mutableStateOf(initialValue) } DefaultBasicTextField( Modifier.fillMaxWidth().padding(horizontal = 10.dp), - initialValue, + value, { Text( generalGetString(R.string.text_field_set_contact_placeholder), @@ -268,8 +271,17 @@ private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit ) { value = it } + LaunchedEffect(Unit) { + snapshotFlow { value } + .onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing + .conflate() // get the latest value + .filter { it == value } // don't process old ones + .collect { + updateValue(value) + } + } DisposableEffect(Unit) { - onDispose { updateValue(value) } + onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index d95afec8ed..c7c8c06d03 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -47,11 +47,13 @@ import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel) { var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } - val searchText = remember { mutableStateOf("") } + val searchText = rememberSaveable { mutableStateOf("") } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) } - val attachmentOption = remember { mutableStateOf(null) } + val composeState = rememberSaveable(saver = ComposeState.saver()) { + mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) + } + val attachmentOption = rememberSaveable { mutableStateOf(null) } val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() @@ -304,8 +306,8 @@ fun ChatInfoToolbar( addMembers: (GroupInfo) -> Unit, onSearchValueChanged: (String) -> Unit, ) { - var showMenu by remember { mutableStateOf(false) } - var showSearch by remember { mutableStateOf(false) } + var showMenu by rememberSaveable { mutableStateOf(false) } + var showSearch by rememberSaveable { mutableStateOf(false) } val onBackClicked = { if (!showSearch) { back() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index 6c254935a9..0c18eb38aa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -19,13 +19,13 @@ import androidx.annotation.CallSuper import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Reply import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.Saver import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -42,21 +42,26 @@ import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.helpers.* import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString import java.io.File +@Serializable sealed class ComposePreview { - object NoPreview: ComposePreview() - class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() - class ImagePreview(val image: String): ComposePreview() - class FilePreview(val fileName: String): ComposePreview() + @Serializable object NoPreview: ComposePreview() + @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() + @Serializable class ImagePreview(val image: String): ComposePreview() + @Serializable class FilePreview(val fileName: String): ComposePreview() } +@Serializable sealed class ComposeContextItem { - object NoContextItem: ComposeContextItem() - class QuotedItem(val chatItem: ChatItem): ComposeContextItem() - class EditingItem(val chatItem: ChatItem): ComposeContextItem() + @Serializable object NoContextItem: ComposeContextItem() + @Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem() + @Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem() } +@Serializable data class ComposeState( val message: String = "", val preview: ComposePreview = ComposePreview.NoPreview, @@ -99,6 +104,15 @@ data class ComposeState( is ComposePreview.CLinkPreview -> preview.linkPreview else -> null } + + companion object { + fun saver(): Saver, *> = Saver( + save = { json.encodeToString(serializer(), it.value) }, + restore = { + mutableStateOf(json.decodeFromString(it)) + } + ) + } } fun chatItemPreview(chatItem: ChatItem): ComposePreview { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt index b2fc5d6da0..09967ca0b6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt @@ -11,6 +11,7 @@ import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -20,8 +21,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R @@ -30,7 +30,7 @@ import kotlinx.coroutines.delay @OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) { - var searchText by remember { mutableStateOf("") } + var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val focusRequester = remember { FocusRequester() } val keyboard = LocalSoftwareKeyboardController.current @@ -61,7 +61,7 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str ), onValueChange = { searchText = it - onValueChange(it) + onValueChange(it.text) }, cursorBrush = SolidColor(colors.cursorColor(false).value), visualTransformation = VisualTransformation.None, @@ -75,13 +75,13 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( - value = searchText, + value = searchText.text, innerTextField = innerTextField, placeholder = { Text(placeholder) }, - trailingIcon = if (searchText.isNotEmpty()) {{ - IconButton({ searchText = ""; onValueChange("") }) { + trailingIcon = if (searchText.text.isNotEmpty()) {{ + IconButton({ searchText = TextFieldValue(""); onValueChange("") }) { Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,) } }} else null, From 1a36c88f72534e48d3972689a68b67f0fc60f16c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:50:08 +0300 Subject: [PATCH 72/89] android: prevent orientation change in calls (#995) - orientation will be locked in calls to portrait and then returned back - calls ends audio session when they will be ended --- .../chat/simplex/app/views/call/CallView.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index b6f529600b..2b0f39a346 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -1,7 +1,10 @@ package chat.simplex.app.views.call import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity import android.content.Context +import android.content.pm.ActivityInfo import android.media.AudioManager import android.util.Log import android.view.ViewGroup @@ -41,6 +44,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString +@SuppressLint("SourceLockedOrientationActivity") @Composable fun ActiveCallView(chatModel: ChatModel) { BackHandler(onBack = { @@ -122,6 +126,17 @@ fun ActiveCallView(chatModel: ChatModel) { val call = chatModel.activeCall.value if (call != null) ActiveCallOverlay(call, chatModel) } + + val context = LocalContext.current + DisposableEffect(Unit) { + val activity = context as? Activity ?: return@DisposableEffect onDispose {} + // Lock orientation to portrait in order to have good experience with calls + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onDispose { + // Unlock orientation + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + } } @Composable @@ -337,6 +352,8 @@ fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessa val wv = webView.value if (wv != null) processCommand(wv, WCallCommand.End) lifecycleOwner.lifecycle.removeObserver(observer) + webView.value?.destroy() + webView.value = null } } LaunchedEffect(callCommand.value, webView.value) { From e54688ad89c2ac3280a8b800e488940705917339 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 1 Sep 2022 11:34:56 +0300 Subject: [PATCH 73/89] android: disable compression of `res/raw` directory to avoid crash on incoming call (opening mp3) (#997) * Disable compression of `res/raw` directory. Otherwise Android crashes the app when trying to open .mp3 file from the directory * do not compress all files in res folder Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- scripts/android/compress-and-sign-apk.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh index c07a640fbf..1bc904af01 100755 --- a/scripts/android/compress-and-sign-apk.sh +++ b/scripts/android/compress-and-sign-apk.sh @@ -26,6 +26,7 @@ rm $ORIG_NAME (cd apk && zip -r -q -$level ../$ORIG_NAME .) # Shouldn't be compressed because of Android requirement (cd apk && zip -r -q -0 ../$ORIG_NAME resources.arsc) +(cd apk && zip -r -q -0 ../$ORIG_NAME res) #(cd apk && 7z a -r -mx=$level -tzip -x!resources.arsc ../$ORIG_NAME .) #(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) From 6727613dc1286ca899df2a6a1255f4758db93a68 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 1 Sep 2022 11:43:17 +0100 Subject: [PATCH 74/89] ios: missing localizations (#1000) --- .../Views/Chat/Group/GroupProfileView.swift | 4 +-- .../en.xcloc/Localized Contents/en.xliff | 28 ++++++++++++++++--- .../en.xcloc/contents.json | 4 +-- .../ru.xcloc/Localized Contents/ru.xliff | 28 ++++++++++++++++--- .../ru.xcloc/contents.json | 4 +-- apps/ios/ru.lproj/Localizable.strings | 12 ++++++++ 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index eedcf78d91..3d950aead6 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -25,7 +25,7 @@ struct GroupProfileView: View { var body: some View { return VStack(alignment: .leading) { Text("Group profile is stored on members' devices, not on the servers.") - .padding(.bottom) + .padding(.vertical) ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { @@ -109,7 +109,7 @@ struct GroupProfileView: View { .onTapGesture { hideKeyboard() } } - func profileNameTextEdit(_ label: String, _ name: Binding) -> some View { + func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { TextField(label, text: name) .textInputAutocapitalization(.never) .disableAutocorrection(true) diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 7ef483a497..3dba7fc0b4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,7 +2,7 @@
- +
@@ -346,7 +346,7 @@ Chats Chats - No comment provided by engineer. + back button to return to chats list Choose file @@ -533,6 +533,11 @@ Currently maximum supported file size is %@. No comment provided by engineer. + + Dark + Dark + No comment provided by engineer. + Database ID Database ID @@ -1143,6 +1148,11 @@ We will be adding server redundancy to prevent lost messages. Leave group? No comment provided by engineer. + + Light + Light + No comment provided by engineer. + Limitations Limitations @@ -1728,6 +1738,11 @@ We will be adding server redundancy to prevent lost messages. Stop chat? No comment provided by engineer. + + System + System + No comment provided by engineer. + TCP connection timeout TCP connection timeout @@ -1828,6 +1843,11 @@ We will be adding server redundancy to prevent lost messages. The sender will NOT be notified No comment provided by engineer. + + Theme + Theme + No comment provided by engineer. + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. @@ -2670,7 +2690,7 @@ SimpleX servers cannot see your profile.
- +
@@ -2702,7 +2722,7 @@ SimpleX servers cannot see your profile.
- +
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 3e0830ec05..4429d3639d 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "13E113", + "toolBuildNumber" : "13F100", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "13.3" + "toolVersion" : "13.4.1" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index a759506256..d0ddbd1bed 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,7 +2,7 @@
- +
@@ -346,7 +346,7 @@ Chats Чаты - No comment provided by engineer. + back button to return to chats list Choose file @@ -533,6 +533,11 @@ Максимальный размер файла - %@. No comment provided by engineer. + + Dark + Тёмная + No comment provided by engineer. + Database ID ID базы данных @@ -1143,6 +1148,11 @@ We will be adding server redundancy to prevent lost messages. Выйти из группы? No comment provided by engineer. + + Light + Светлая + No comment provided by engineer. + Limitations Ограничения @@ -1728,6 +1738,11 @@ We will be adding server redundancy to prevent lost messages. Остановить чат? No comment provided by engineer. + + System + Системная + No comment provided by engineer. + TCP connection timeout Таймаут TCP соединения @@ -1828,6 +1843,11 @@ We will be adding server redundancy to prevent lost messages. Отправитель не будет уведомлён No comment provided by engineer. + + Theme + Тема + No comment provided by engineer. + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. @@ -2670,7 +2690,7 @@ SimpleX серверы не могут получить доступ к ваше
- +
@@ -2702,7 +2722,7 @@ SimpleX серверы не могут получить доступ к ваше
- +
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index 50ec87db4c..ab6a47f150 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "13E113", + "toolBuildNumber" : "13F100", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "13.3" + "toolVersion" : "13.4.1" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 64a8b5b201..bed8f2d341 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -404,6 +404,9 @@ /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; +/* No comment provided by engineer. */ +"Dark" = "Тёмная"; + /* No comment provided by engineer. */ "Database export & import" = "Экспорт и импорт архива чата"; @@ -824,6 +827,9 @@ /* rcv group event chat item */ "left" = "покинул(а) группу"; +/* No comment provided by engineer. */ +"Light" = "Светлая"; + /* No comment provided by engineer. */ "Limitations" = "Ограничения"; @@ -1232,6 +1238,9 @@ /* No comment provided by engineer. */ "strike" = "зачеркнуть"; +/* No comment provided by engineer. */ +"System" = "Системная"; + /* No comment provided by engineer. */ "Take picture" = "Сделать фото"; @@ -1292,6 +1301,9 @@ /* No comment provided by engineer. */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; +/* No comment provided by engineer. */ +"Theme" = "Тема"; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; From 313963dab6107dfd9928c4c50f6e26d48db956cc Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:54:34 +0400 Subject: [PATCH 75/89] android: fix incoming call view (#999) * different implementation * layout Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../app/views/call/IncomingCallAlertView.kt | 17 ++++++++++------- .../app/views/usersettings/SettingsView.kt | 9 +++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt index 31e99eda96..455fc915f3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt @@ -46,15 +46,18 @@ fun IncomingCallAlertLayout( acceptCall: () -> Unit ) { val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight - Column(Modifier.background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { + Column(Modifier.fillMaxWidth().background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { IncomingCallInfo(invitation) Spacer(Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White) - Spacer(Modifier.fillMaxWidth().weight(1f)) - CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) - CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) - CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { + Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) { + ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White) + } + Row(verticalAlignment = Alignment.CenterVertically) { + CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) + CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) + CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 1e0a24beff..ac1d1a557a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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.* import chat.simplex.app.* @@ -342,11 +343,15 @@ fun MaintainIncognitoState(chatModel: ChatModel) { profileOf.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, - color = if (stopped) HighOrLowlight else Color.Unspecified + color = if (stopped) HighOrLowlight else Color.Unspecified, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Text( profileOf.fullName, - color = if (stopped) HighOrLowlight else Color.Unspecified + color = if (stopped) HighOrLowlight else Color.Unspecified, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } From 79d891e5bc90cfee8577519aff57aeee996394df Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:06:34 +0400 Subject: [PATCH 76/89] android: version 3.2.1 (52) --- apps/android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 8d439ee0ce..eba863a3bf 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 51 - versionName "3.2" + versionCode 52 + versionName "3.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { From 0244f2da9af91cb7f3d3d6ae3a57485be659bf0a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 1 Sep 2022 12:42:13 +0100 Subject: [PATCH 77/89] blog: v3.2 - incognito mode (#996) * blog: v3.2 - incognito mode * typo Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * add images * images * fix link * update blog links * correction Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- README.md | 24 +++-- ...220901-simplex-chat-v3.2-incognito-mode.md | 97 ++++++++++++++++++ blog/README.md | 20 ++-- blog/images/20220901-incognito1.png | Bin 0 -> 337526 bytes blog/images/20220901-incognito2.png | Bin 0 -> 513044 bytes blog/images/20220901-incognito3.png | Bin 0 -> 300876 bytes blog/images/20220901-onion1.png | Bin 0 -> 444925 bytes blog/images/20220901-onion2.png | Bin 0 -> 721228 bytes 8 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 blog/20220901-simplex-chat-v3.2-incognito-mode.md create mode 100644 blog/images/20220901-incognito1.png create mode 100644 blog/images/20220901-incognito2.png create mode 100644 blog/images/20220901-incognito3.png create mode 100644 blog/images/20220901-onion1.png create mode 100644 blog/images/20220901-onion2.png diff --git a/README.md b/README.md index 7e2e6b5fb5..57dfe56dba 100644 --- a/README.md +++ b/README.md @@ -79,18 +79,14 @@ You can use SimpleX with your own servers and still communicate with people usin ## News and updates -Selected updates: +Recent updates: -[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced netwrok settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.md) +[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md) + +[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced network settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.md) [Jul 11, 2022. v3.0: instant push notifications for iOS, e2e encrypted WebRTC audio/video calls, chat database export/import, privacy and performance improvements](./blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) -[May 11, 2022. v2.0 released - sending images and files in mobile apps](./blog/20220511-simplex-chat-v2-images-files.md) - -[Mar 08, 2022 Mobile apps for iOS and Android released](./blog/20220308-simplex-chat-mobile-apps.md) - -[Jan 12, 2022. SimpleX v1 released: the only messaging and application platform without user identities](./20220112-simplex-chat-v1-released.md) - [All updates](./blog) ## Make a private connection @@ -177,10 +173,14 @@ If you are considering developing with SimpleX platform please get in touch for - ✅ Chat database export and import - ✅ Chat groups in mobile apps. - ✅ Connecting to messaging servers via Tor. -- 🏗 Dual server addresses to access messaging servers as v3 hidden services (in progress). -- 🏗 Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (in progress). -- Chat database encryption. +- ✅ Dual server addresses to access messaging servers as v3 hidden services. +- ✅ Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (ready for announcement). +- ✅ Incognito mode to share a new random name with each contact. +- 🏗 Chat database encryption. +- 🏗 Links to join groups and improve groups stability. - Disappearing messages, with mutual agreement. +- Voice messages +- Video messages - Web widgets for custom interactivity in the chats. - SMP protocol improvements: - SMP queue redundancy and rotation. @@ -191,6 +191,8 @@ If you are considering developing with SimpleX platform please get in touch for - the server doesn't have information about your contacts and groups. - Channels server for large groups and broadcast channels. - Media server to optimize sending large files to groups. +- Desktop client. +- Using the same profile on multiple devices. ## Help us pay for 3rd party security audit diff --git a/blog/20220901-simplex-chat-v3.2-incognito-mode.md b/blog/20220901-simplex-chat-v3.2-incognito-mode.md new file mode 100644 index 0000000000..f20b5a8f77 --- /dev/null +++ b/blog/20220901-simplex-chat-v3.2-incognito-mode.md @@ -0,0 +1,97 @@ +# SimpleX Chat v3.2 is released + +**Published:** Sep 1, 2022 + +## What's new + +- [Incognito mode](#incognito-mode) +- [assign names to your contacts](#assign-names-to-your-contacts) +- [use .onion server addresses with Tor](#using-onion-server-addresses-with-tor) +- [endless scrolling and search in chats](#endless-scrolling-and-search-in-chats) +- [choose accent color and dark mode](#choose-accent-color-and-dark-mode) +- on Android: + - swipe to reply + - reduced APK size for direct download and in F-Droid repo from 200 to 50Mb! + +[Implementation audit is arranged for October](#we-ask-you-to-help-us-pay-for-3rd-party-security-audit)! + +### Incognito mode + + + +_SimpleX is already private, so why do we need an incognito mode_, you may ask. + +You indeed can choose a pseudonym as your main profile name, but there are several problems: + +- many users want to have their real name as their main profile, so that their friends recognise them. SimpleX objective is to provide anonimity from the network operators, but not necessarily from your contacts. +- even if you choose a pseudonym, it would be used for all your contacts. And if two of them meet, while they cannot prove they are talking to the same person, as they use different addresses in SimpleX network to send you the messages, they could _suspect it_. +- any pseudonym you manually choose leaks some information about you, as it's not really random. + +You could also use multiple chat profiles - currently you can only switch between them via export/import, we will make it easier very soon! But there are problems with multiple profiles too: + +- if you make many anonymous connections, each in its own user profile, you would end up having too many profiles - it is very inconvenient to manage. +- sometimes, as your relationship with your contact evolves, you may want to share your main profile with them and have them among your friends - multiple profiles don't make it possible. + +So, the new Incognito mode allows having a new random name shared with each new contact, while having them all in the same user profile, and without the hassle of managing it manually. It's like a private mode in the browsers, where you can temporarily enable it when you connect to somebody you don't trust, and then disable it when connecting to the friend who knows you. It can be turned on via the app settings - see the pictures. + +I don't know any other messenger with this feature, and I always wanted to have this mode, so we are really looking forward to your feedback about it! + +### Assign names to your contacts + +You can now change the name under which your contacts appear in the chats. This is particularly useful when somebody connected to you using a random name – you can change it to be related to the context of the connection. + +### Using .onion server addresses with Tor + +   + +We have released support for using SOCKS proxy to access messaging servers via Tor, but previously the servers were still available via their public Internet addresses. It means that while your IP address was protected from the server, the whole Tor circuit could have been observed by some actors, and for some communication scenarios it is not desirable. + +This release adds support for servers with multiple hostnames - all servers provided by SimpleX Chat now have dual addresses (one public and one .onion), and you can have your own servers available via two addresses as well - all you have to do is to install Tor client on your server and register its address with Tor. If you server has both public and .onion address, it is not really hidden, so you should enable HiddenServiceSingleHopMode to reduce the latency of connection - it protects anonymity of the people who connect to the server, but not of the server itself. The server address would include both its public and onion address, as you can see in the server addresses in the app (in the contacts pages) - you should use the same format for the addresses of your servers. + +Both android and iOS app allow managing whether .onion addresses are used, and you can also enforce using .onion addresses - in this case the app will not connect to the server unless one of its hostname is .onion address. On Android, .onion addresses are used by default when SOCKS proxy is enabled. + +### Endless scrolling and search in chats + +Now you can access the full chat history via the app - it's embarrassing how long it took us to add it! And you can search the messages as well. + +### Choose accent color and dark mode + +Many of you said that blue is the worst possible color, so you can now make the app buttons and links look like you want! My favourite colours are green and orange. + +And you can choose dark or light mode independently of the system settings. + +## SimpleX platform + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy-technical-details-and-limitations). + +[How SimpleX is different from Session, Matrix, Signal, etc.](../README.md#frequently-asked-questions). + +## We ask you to help us pay for 3rd party security audit + +Our great news is that we have already signed the agreement and paid for the security audit! + +It is planned in October, and if there are no major issues we will publish this report straight away, otherwise - once we fix them. + +This is a major expense for use - over $20,000 - I would really appreciate if you could help us cover some part of this cost with the donations. + +Our promise to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We will be establishing a legal framework this year to ensure that it doesn't change if the ownership of SimpleX Chat Ltd changes at any future point. + +Please consider making a donation - it will help us to raise more funds. Donating any amount, even the price of the cup of coffee, would make a huge difference for us. + +It is possible to donate via: + +- [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us. +- [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission. +- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index fd6bbdb853..b862e3074f 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,24 +1,32 @@ # Blog -Aug 8, 2022 [SimpleX Chat v3.1 released](./20220808-simplex-chat-v3.1-chat-groups.md) +Sep 1, 2022 [v3.2: Incognito mode](./20220901-simplex-chat-v3.2-incognito-mode.md) -- finally, secret chat groups! +- Incognito mode - use a new random profile name for each contact +- use .onion server addresses with Tor +- endless scrolling and search +- choose accent color and dark mode +- reduced APK size for direct download and in F-Droid repo from 200 to 46Mb! + +Implementation audit is arranged for October! + +Aug 8, 2022 [v3.1: chat groups](./20220808-simplex-chat-v3.1-chat-groups.md) + +- finally, secret chat groups - nobody but members know they exist! - access to messaging servers via Tor on all platforms - advanced network settings to optimize traffic usage - published chat protocol - new app icons -Jul 23, 2022 [SimpleX Chat v3.1-beta released](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) +Jul 23, 2022 [v3.1-beta: access servers via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) - terminal app: access to messaging servers via SOCKS5 proxy (e.g., Tor). - mobile apps: join and leave chat groups. - optimized battery and traffic usage - up to 90x reduction! - two docker configurations for self-hosted SMP servers. -Jul 11, 2022 [SimpleX Chat v3 released](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md): +Jul 11, 2022 [v3: instant push notifications for iOS and audio/video calls](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md): -- instant push notifications for iOS -- e2e encrypted WebRTC audio/video calls - chat database export and import - protocol privacy and performance improvements diff --git a/blog/images/20220901-incognito1.png b/blog/images/20220901-incognito1.png new file mode 100644 index 0000000000000000000000000000000000000000..71bb817dc039cf28032418a779edbc63fe4a4f65 GIT binary patch literal 337526 zcmeFZ2UwF!_b-a#1`z}m5s(gwjQ}B(gsLK-(iEggGlYQD00BY^*btEt5v3?aL_~TI z9i)aDM5zfq^b&d~q1@=+?(MhF{_g)f=REg5_uS_X&&xZRnN?=3^_y8U?;C<|>1Z51 za{34z9o^9z*RSc((f!s&yBrTQ&}x3WJ+wu;9kRWmeT9xL55v3z{f+in5O!Tpn~u&$ zl#cGf6FRzWTGfMTIyw(YI=b0=bae7Dbab39ab>p^X${kG;~O^G+H@Cb<->H02Tst@ z(@FRFECrA=>>r zZ947x{?kXhzJLC4zhaHBf(zcdk8npI?;~6URaFJ0B!LjY^V(3Do%8p~3v^PFQd07g zKzT_SL7=p}w4A(@6dfJ?i$i}(>3VnYr}R9h8KPwQmWkBHZADvf`5BlETVI1O){Z z-K=fo^{%P^K~DRmBz)i9-9=tP0);|}qd?*aH(LoH1Okzel#-B=0?>K@klxPjP%nTp zQsj3gKlxmPBVlfKF79>+XTk4$p;ibFcO_xr?}GmP`CU(I*q?G-Jlq_ANU?@Vz#ZXE zaA$X<1W+6(@lSNL(fjQqnIG`mQ6L0xD>lK)8LZfEn?u>OVWd&|EigL~Qi8?x^$ z|CWrVqPF(`y^>B&|EMR@UConbuRrYnFN*#g9`cU23tU1EjzoC4!Qg71G-HYUlbw+6 zdhmav{6Djzb^k|3EX!xm4ts{^UtaA&zbK#%@2snBM~<4D5x9!iY?7i|5>>I6X9P2_*2Fo0#Nti zG#k^_nZLy=KvM2U)c?-yN3Va+#LgM%4t0jXZ(LKR*+<;Y&RQNUEp25j4Tb`2Y~Wyk zw5&7`0G5%F1lT~WtYoAia2OO$!_@EGeez^5d(m!hdPNpdFeN}O^ z`#;JRu=`MFTe!8d#LvO~toZxr{tA!(!FwQDt{^WX{lDuyNwAEyq$ErhU?WF!p|q70 zgyub{6#!->1Cq0rlD3wV`O^jeGu}f&J^w%GJ1LMj@Yk&Er~UsGeE;4HKmGK-;JCj} z^gkDy|5eyo*+AeB7|l~wAgB#M8U}^|pg>v)Dh0KXlm=N_0c~JEhxdPi<9?zZ;p_=_ z`>%2T7cyw`ER>ek!rhR{Hf{(fL8yz1qaEzWQYYc*Z2d<*_-9fg=#CKlCHeT>{J-${ zyYv5_m|*@Gt2Nxs?!QLKKaLdk(@cW@r;PbuA@=ul?*B)?gh^V-fgn&RfSeQv2#}Tn z!2u8$1PG7<+DJpdK)8$?h?af)&##C7>oe-_A^flT%8xu#Tl*jKoA0SNt$~&u+S)lo z9j_r^9^dz0f2{b{|M80dZ`u5Xus=jy`yB`B-_iShHUFa`;-f~0O*g3+Lk$M_e1#hUS$^s#>vH&S@+V<%$?!WZ=gU|13 z|8j`)!(hMH(vFCfkI?+_^SDX*2u5azc1m}d?#8t%cf1bFSN1QS?T;GvaO1xi^v2m> zFpd0$-ub>re=S>IUlhPZouU37aQlGQM4$>Cf5b6~>skSH2ZczCmk$E_B(U(ZD~C<{ zYTe5R2RA5uvptFvw;-##nMKvd=jKCKTa)th^Dp+|Lkl)6=E|S$hHb__E6|POD>sj} zdB=U9;myPUzWuie{yPf(8wmeB7yQ3F9j4ZQ&PzR+S|?54{@Lj9fg$$3a0i8?g%8+o z1RXmC5EhvZQ}A{}QHCn-C#rpvl`*}C5l{&?@6*)asZI?Sz{n2;g!6@X+1nb^Y2Mb@ zr{_GSXFb(N_96hj+-HH=p*B2Gs@D`+fz4$2$=|{rU4-8L?W6ueW=qbn&CJ@K37pSf zBM6PZu>Rn@K2u@OE7@dzLRA8H)!}ugLjIw=?&00(hQ-f@c(STQjK};2^eUZ-NreQ~ zwV6)=InmHm6Nt;L=l3&l_UaLh-#?bpXq|BYeiNt0l`; z0T0=guNyf>8AZ-I^qU-Xgl6z9C=q#AU7N{xNRR(KIJaeLFaG86P*nU^1C7?ani9bb z*#cIxras+#=88AtxASI)riQCJZx0l+Q8gE~@9sWI5iCI8?C~O(%l7Otb|V!x94fLD zZMa<>HHkj03y3Dh4Ao7faJh8C>}C9YKV9j#cs z;_N$hFMi@)dK-9Lyr+DTce>nsS7_kC{(+v5x_qJD?} zR8^jV1<`aHU{aLk1T!Dz!#!U8O%o#Yk{l>St+;DDpI%zMp4+Edx@9(Q+*lf3ym#T; z9J#~h^@F571ckNwbVq3)?&x%jX*-<4oJ|N^ILXR0Mc(ck8Lk3e@zh}D6LGr~(BWyi z?)}?u=ZAd?USG$x487n8kdc^zIo9*@;R?dbCgZ&)OAifN%^9bU-jATy%d;GhX#H%; z|86C}k8>b_MJzb2i$pAyXWdRF*j0b4y5%tLKVKg_Xt}u>;JlH#*Cj9)z2mdUQ*$nv*xh!J~~*3_7H_8E&z`5lXoGzNzRCH)FC5$N8cAe6G2s8Q+iNn@Z|QT7e1YdCCcY~Hq&$!j^#D4 z1A?9BLt^dY4Vwg2{qU#=<^X>J+0UsJpai@{Lvd{o{ zS@N^B@%Myi(+BicZjm~!(YI? z2AU;H(qpk87}oJ=pxzyK-V*F>pSICY3%qr`kJL`re6x>$J+Q1-(=E25Y=_@_Hvc5- zFy!(V&ESe|P_*W&&n*}C8TsQY%lV2d-eXS$K!Hmly`Ne5Zf!g~G3>3Uq0GU~;1f(3 z+CaxdE;$I)fApH44GOt&YWXbB{X)$&Z{a5eP4~ttHZ8;sz5?QyJ*jTNx?y(kn|hMMM?j_RlAdzw0wi(4y~(q6&E(o&HS1QIM6>OD{Gn(dW@V3?-KQXCyl7B*E`e2HHu& zkzhy*gHy{GEdR#B8{(satLtN?ExexZ6{{ZT_&H8+fHxU5FO-UnzcY$mKx%0o6gmFl zf@z~&VmN5$p?luq#A54_?rX(4!r@*Vi6-_o8n9rBdR&8YNm6vFIBx79xNOwe)Z4^J z>5$Rkwuf(okei*1Qk+y`hztc#)_^KpU94Aj1d+7T*Bg!EJdl1HPO9d}XM3*%^1Z6* zxbJx%NY1xaIALu4I44Em)K>o4g42BP5>vvGfK&sk?>c(^xAUP=a&z%h?~P!~mXveb zN<4FMPpF>b$};e!i1>9Gs~gcfDT-Ql@)4aZp}43ol`#DKN}qAgQq58Dl2r6(@bImh zdiPSe6-k$3@uPt=lFdjSQf+6M<<*hCK9J`zdZX1N`>S4c@t-%(AE?Q%+}n02=d|>@ zu#=#K6t&mFC#poN)`aUmxj531sFE1J;&T%~2$Z?eYrUB|%R+c`@ z-909>UGK{6gb9=1J8+?FL@z%s(ukhdr}ESN>;&22q-B{mWy=!v6$25*Z*6uL7s#Jn zHC}weq_y)_ENy3NG$T196q_f@%>3U^;${OrQK;#|9DqeTUn6$1z^4vMnyhXGs52b> zV;*<|`ocbGxymbt>oBZlz zrn%NKl4-A6+XDyo=_ll?eHpg>Owe#(9Jzml&W_pkLIiB*=7}-C<`9!{@j81pn2|{kV@+V@ zJwFx>P7kxoAAQCjupXxW@WkuNA=wokpVebt_bxu<_vgK&XKO~`DS=8}o~e^MAROpE zURbyBEa~FS$rnq77Pt9HF0x{oC?hmRYg^JHPxJJFTHEj%?g0U7y5x&E{ZNq&%}q1j=tG>`brve%qefJ- z>oFt?sT*7~CUUcv-2!y?*c3jGoXUN(NtB3H$?sCDe{#OJ$#wYNTA^+eY+hlCy>^tj zdk+ZmYn8MLGrKY|d?4FFfm$)vn-^6*Sc94+y?%1Ayz7JjFFn0++sb0}CKbN)xNjzd zdr(q$_ZGL_3fD~LWbM6nYQo0;X^-{DoyMRVXvS>SOYf%V>L+E~a9T1gNg+1IlQB>2 zJB=(l)$}r)Jdr9%o+fFQ=*Q?=+|wMgQ`Ncl2;)y$B}*V{<%In5$v1IGv3|v5M)Asz zb6-6{5mpx{Z;^SF14T$aNx$1(5_O51la&+zY5^wjwn=qLx8+S(ffn=B-IP&uXibD6 zwNM0~1Vc~)0dLVMLERYK)Ii2i*+o;ghXrX~imj}1Ywny{o^Be55pvzBwgikYDiS3* zL_FW<9 z2Fl^xG1!5eqjOyJk?yO;hU2?{+&=vQtV%L~?P{uOE*V8KBl>4#$CMr&f^=qYZwD2f zV+22+$Oh!dl$|FDF6a3Qs1LN^j>lx}@FJGchw!c!dntClbEmez2`3f9lhu?dNek(S z;;8}0kfE~YDdr?b)5irbE^)>z@1f@VbI^Cm#zom&R9C;*7h$`X759jV>&O6Xj*l{| za;s7fHN4YPTxUeSw>Yu%DS902(PV!^EOB|rZ?%0eXJyI zkVC2Z1H;L!r#EiA3m00i>-e0G(kdX9^=fV{tBlWv1^a40!b#k2Vve6vK*fN1$NjZ( z%F#vzGpHWGTAC(gb6`5CdMT}^(zkbf&gm%i+=<=m#Kj}qzNdS13oBE?n%5l&CG-j9 zqvkJ-H_Du8o4uSv^h(gZ*L~XY$Z5SBPrbA(meaYBlA7)kz^qi*pyJ zgU;y;P#-#zdk$?aN0;tXKZur;-jYu%Kd3W%HK?5ay{9R9@F2er$?7X_<@m~RfyvGZ z>&
auiZB~Gn-efN9An(0Kp078QCR<;nXn2;a;#)cd}2{`!q&8ya#|2!e33UETr0J%b${0`?(_WUm!m8Tu z2o+1VxVhP3A!*FOzIoPm-La&}x;SEZ2pn};U&N0ku`np{@XLqa1PPZ|Ss$G)F94vY z#ADxvaP4<;mpzLXX5mcimR$1T{-~ZRxbYg3`X$Uvaqn(2W+0FWbv6;+R{NyA&llg! zxqJLEC&q+MO2bn9#a<9b#@n~9W_wh8AS*ew{FS`p)v*&M>#yQp9kNE4^?itjn|aFb z0BY>%n3Z%k(&$s=DT#5{l)Q3T%$&X%nXJ6El7O9FnZBeWIped!Gb;gxGSFCp0; zheOP!HwAVCbD&Q#O|F~8N*-aR6V9M5{;2>a;nmSP&qgIffaRHxVcwb92AD~Sxq)w1 z@=7j#r{4u5z{JN~={@}1AFmgQ_~^m0z&IlE7(2&oqLWgpGf>_kB4qu(@3Zq(=vSXK@WyG3F1 z;;E&kZPUA38WynO_Mt4K)9!$VGdJX$=dkQ)mUmq2Wp@>Ih;0-DWmraHG6ByVivHp$ zo{=!H0UVheixbQ7PtR;jcOr$Qc(R(70OFlM(M!kP1BM&%MHq~|KIw6>v!=zWRCeR# zj{^JTs+8sx&1eZ=lR^A74S_hc&@5~G&dny(z9pd5lQwK!c+7Fj^>|>Z z|5UbUv&V3$^lca?`(#scR+TfbI_qfZEbL{q%N%7vV5l^CffBD0MyABP5{O(V3*04h zXs7R+dGB2>F5a(l_S#1;JWph59Gn|y#t+UK;U?i$+u0t+zou||^>CeEeBW``K3*5J zRAl==A~WWgE%!+>OTc3X_IO;;nU>{fchDwJ*@G=|GYMCn9K47~0K4v-U>`fD8{fj@ zs_ma>m4Pmjamc>mru|A2$H=&?^>iwJ4ARUOIo=c%>;}E>K%y7-=+hUmeLv{50cUIU zgz_xwoyx1ZsqR~)881UAxh%rJU7{I(ckyPae~5CuT7X|n$#A%57g}k2-v~e&S=)m_o-j0-NAXpAJuN?c}84>ho+oGfJLp zfbBXpYdy4>6W#1J%h$6_au#2~t6dqdFAW?@LHis@hF6P3n$Kf^o^;_4#aU0xz9P@K zX%;>EorhB841|KQy+0(-;^vG&Az&;_{0d>EFK$kn6|u!g&t_iCH~W{c_<| z$1@qhQ>jXlB*RE0sk&?i?(kZ~UJA~#LeTdUaOj~$x>D$ewF`K8-%A(0b!JXpx#~i~ z+^361TSwl;0VTjV%_y0TJh?~D4KOI%VsZ{w6mLv7%Uc%4*Y?@R)z2Gg?yT_|KIxO< z^6z1J5~8z9ENDBR&KKV8sZ<~SX7p)d(~a!t%l#uVu3PeEpYlKYBm>l=ylSg35z80Z?#BezJE(HjY#GwQ?N;jijkXWy%= z(8c*MiAA!>MVqGxE?4DUOF8h|NKvcqdglwwVWw!Y3XBm;3Tmjqxg@bOgKvn@AJ}oL z_H*&j#x1cJEx~ecKUR(eN(cwuJ5)vEZA)kp<W=B4tp4?-g1QLl_*T|-l>gcIf?KuP@UbNX=#O^f5!kjUA-;jd*&n-y6_V~9bU zNV9y?kuPZyySDjgBmaPl`vLFrs#Cmx5!uHOA&)FWk8-om4Wv2Scg=r4lhBPkFvMK` z{cw=8l`ggA+?yDiQ7=Q`q) zJ!WIDF)S(`+g;?fv~LRbx=(N)N<}-A^4hJvbY4xZT+ng>#p0h}&vnftDx>dKjYpR` z-V;k^HN_ca#f^sq`-pbqtaoBAV}f1WZnLty+LPaEf3g|F5-lZO@BZ+q*ym1sv5>^5 zFsIma)|kmpl9K@zhwlQ#d(Vbx>U|*`5q+>V7IN9g%&&XzvUO3#HlhCZg4}8gII8~> zg{>$oMfT~Nv2rPZ#w4)wqszl1cz91aLV(PpYYs2OUAxgVn7x@L>v?jDT(!U_pcSSu z4Ch~*Z$*g4pD=vR#VNHWyNJM&*d6@da=o}332VF-QTjeoYvV!`2^5>`mYtL zU?mCYuf^FtO-ka_H+gFgz#;$1y;G-RU7dM?1a5YxT2J)u-ZfGfCx6SXx?hloIp5P> z?5OLibXm6E&@rfN_v?ycb%4NB1__>F35Ga|PD0Kz{BN zE4`OFP5tM)dnnUw7|7+R>UjGQHqLQMP}|;pVxyBgf8@dv#;=vtWPQ}X;fR}M9-^0_ z1mU}pHYgi)n!90hK5HeNUf)~Q2wk>7@Uf?A$gMt3kRo>b3mdAC1=5Qh*`RX|Z6Pa|#sB7&s4L{>#pD%WHaNg|+ zN{`VB&}E+!m}bttRK~NHO6cCdth()VdyPskUI}E3ejX^^ei^i+2Ku6z@7KvvygwCj zAUnJSxufkAaIKdw;3}s~I;Jv{iCmDX8xPJ2UR-?>qwG65A4^gU4ZJZA>e4dIK;Mld zY57RBrq!SE7+MbtZJyIljXkN7axO9PsHr_-3Lj6*i|_WdXpCdeS&ynR~1sQ$|-X=-ByunN`~Z6@g~TXGEvr2?A9F8 z)AkL>h-lqhz72x}nW!gnQS1Wnf&N8BmGH)L%n;UIohVzMRFYV^Yc#iHJIryLB#26s z#eys;K?!=x7y3I=XVGGUAQfyHxb+iTL&1P28K%Fb;T$@5k=|0Xdy6Y~h(hMkG-n25 z(@{L7I16}RhyU!Jnk{-g;Qs!DB(OS#Vi`}WTAQ|5xAkqE_7x!gR_a0a9qy{g5O7(; zT-nZ)NFJiH{A*o6!M;)4yYEF6Dj$nF<82Bo(cjaA!B2VB~&s_dqQ z5%Q{f4S(sX8-DtHxGFL4lewdj2iDa*kFr?O%^J7MEl2&tH8;rwlQe~?Ss+svy?P)5 zRCS&HTJPaAR_H8a0=@&LKDpe!)fckrBJlRP$8eErwyp0t$DC(Y%^d{-fAU1vc(`8% zmo49(Ltet0uraQ}FgX<(%Htg*Pi_ zlibHDyUfIh={4Po8SxWZ&mt`+BP=e8uU zhZNWswXm83k#n?_Z4^I_wMktJWpWoSL)>?xQrdZ3al1#gKLk+vZOeIja~88RKB9LC zMS~veb_Kxx@S{BJOAr(BeXSlOW0}2`)C^hS8)B%;`|7R{!q1ar;HXTUSJ#=$K$3dG z9OMu~rJhv$;GEBknROUyaTgu784w+^uPL&_NKpRT_>i{8xX+LvNxwy2eG-o?w;6#z z9~3AWok=ZR{@kweVJ<`^BP2{EC&+Pcl>HngvD=(P(sACE4i9z^&FhfNLyL-*>dxSu zh7Tk}cEj}i2DtHu~nfbm{okLB5XGgmU7}-H@242ZfwHG zcAtoLEw=`;IS>GBBi4`Pqi)_4ep{O%ad#Ry8nIj9brqw4?dO@*~3sm!4{w9lpWA`!1<;}2H z-((5%+((u*T8p)J#x7o5jRw!3W@vBUxE4K-l z19Zkqb^fY_^Eps#%t^V7^`xFR`qqWx`7NB)L1qKzbcr5jOt~6glfS4KvmR=%gtKLM zG|Eda88{bHH%$`vy!R*SPV^O7pToT@={ox2;8JfsA`*9)k!p2CMC)n%>l7aUZWP|m z>9fCjiK%PaTPnBYi-T?cFQ&GvA=zKG??vXQy<_mI7$3guevIjpCHw;C4izE7#eHhh zYNt|yLfv|TsVuHti+1grtd@0aaNPqurBp?COCcM*_%QGo;OPMqb++NP)m@=ONxFP1 z*Xf8{DB+d3IH!@(hwtno+-^QqPCxE&KPqpOv(lng-27U=2brC~$U^)*D9-rNTPW{^ zYqI_&fDuL8ul@-G6?LpzCup~D=@H-A?8tjB9NNc$92NP)ui!38JxWo` zXXftqvC*B;kKbCHn}H9!vdxC=r|Dxt9v)UK8Hcuutdk73;aqgxAT|nn^Hjp!P+ozN9K9+_ApV70^y6f!Om ze1l{&3)K}Jb)IN3+cY~)SK4jS#n+rM@mxe4a_e}aKlvQRfnOjtBEVe(2@d7UAdt@FPJI}np2V}Yyjt+tSi*1ny4GfAb7Fyqe@4z`e(IvkOk9w| ziEFy?oF(INJfuQ#!L>1Aqis46`7#UZ$|H8#W237UUP%$%dYN^iqUJ#ip?Ra)K9->$ zwGTYeNE!>U;OgOZEI!0{RC2!TJnKi+*s}7TQAHd?%WB@2^@C(v%t=70gsjUM(>6PK z%daGB9Oz~-rO zeG$`S`a@o zfLT*-lYA##{*$0??IRO7+({UU2Oe%TWUZ>OW9Ul|KcZd%CJ3{6#C?v^evn}`f-^p~ z&czIsV!Uf2l|YEVMM=yYx*L9lC-n4@$c+0>^f3?-t!#~Fk`hk2I!WYHkKTe>A6t4X z-PSDQ(PMn^0`j0K7`q4e&*w$k$E5_XZIA1F@awurzWi)`VfI0D#TkzM(XUJrpF4Ab z2MoNJ;a6*LIu+6HKb3&_g;mAI0_y2grOeWRcS&5&=p}?=_3bc|Cv3UT9oY&lrkoAF z#`tu(zdHV!KeXfwDeme7Yj%+Vbv{*?P?99}RQQo=4dd(an%9RkmGxY8>ds)Ilj?N) z!qLL5U8xcRYQU|}F`T>4f_Klyp1bjBWcAqzLX+)C+Si(5{N2s9BiCv+c+^6|QMZz4toyEXh_xwj2`*o|C25Cq?E9Da9f{+48&4I5|v=&-+w;*xiqwc+b2ZRe-j;cWF>VdE5b$oYp| zOd{qi3hGz;sCmFI*X7Y4*?e1E;mR)O-67a3YRb2BwsS=-OSl_K2&t8!u)+-GOwI?V z&;wEfge8?ee7xK(($}xgNkoj{RxeH-LAZ$6<2UCYQ?x)s`Vf6 zKHl0>7k{|!m8W@I{#K&6L#oD6=e|4HQNTbvRrge*g2Y-;e2uqubx3$gY%(g0isxq6 z$k&})sT7B0m1ZeN$2Y$depw0E@@WMpCtu&L-n^JiDE=EQ75D6v_L(yIGb#ggz zxoBRai2bFR%hnuA2?r|NsoHa8lgua&*uKWG z57<*@FiglORY(pKLbNLOaJ8|VQuEGLtg}GU4j;FOvw1pi{7-#kmmIz~S3eu{a>+@yB$r75V#3c(+8%kP zD=s*rU%mhn5f^V3&FR0&n?F6(U}WW(XGuMp79&A7=O$HKsfM>4(evWY0*;?O0!l_&ke6c;X80UoT$wPuR;2=T>G*F+K)%pa597fw2x zXDxJ;8uB?!Sq+To!s0ecJfN#|{PP{l6i0yKomM$qI5H!5cH>J-sLD-5JZz-5Ducyx z-MA0|b5P0UMIs!Ta)Ki{alI1x#6>i|# z1m>hxMk}A)(bqtrA>%N7kUzlJ-yyWqB6p%Mx7|={3E9?wd#FBP#XRPPA4KOy87Ry- z@H>}hV;hYG+;{_+4JM6EH)nfgqaGG#k({@32hn-`@KP$h+eu4gX2TRrnm2Vjb73|= zoWq+>LYB9+t2+Uk*LhJfB^(LATq|z6b$a>!n5v*aH^mZtP7IdfQAN=BqTm+0Zi?lf z3-!<%r|@x66ZW2BcMe&UaA~+>Em`~VZX0PAOn15}ir@F+qQpI4;N<8Vy=<2)0>%yv zCYfan#(f)*i>f5U==%rc$hVxTT|-%3K+N1>0ZG{TTRch==kaWl=S?$Xt^FsujXWi) zcE%ENPV=aRO?Yy#@J&TwRJQMLgcJ`2uWA(~Ex3L)WDM$D)9miWu4{BRmdi#d6V1}a zx?u&Q7^ya>9UpMl3|$KZUPj>8nKTE=!J?luSSkU*yVN z+4Xv+m6l*1t7Qp{SoOv;p5;in;INKKKL(6KlgVtL!Z(%H@-MOAUQP}_ljY`fiJ!9e zUhC?o278L)V!bx_CU4`;$fGicxmxR8IDK`>=4!G~($Ib6VDKJF=HZ^HPtI=ekaK`0 zQ@*G?`^-S_(D%I{tE9`m0{+0gkhc$~o*WqXGk9>-Sbc&okr+MO|J7Ro$M7?)c`CEMG2qiZ5H%rhI*gcZTS2AFRSak# zRGd3xqlh*XsBbSer^o|8y!$+DLDh%Yds<;hyr)}vAz89f52owNTA)JB8}Q(I9X*R1 zQ#D@Gm~q^HM}Y9iZ2*STmBiig4Y0o2j(q#a64bX7lxjF8lh8SZz%5>ww$~xkL?E_Q zu?FewnZ*I);Q=T8v58d5?Fkw6*S*+

*V>aC?>WQ^{-fZe~_sidY7E%8JWE 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�$Z4~Q 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 z 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�*V@TjR~P3?5iK4~#;^!VeY{U)Ea|C!B`{*VRxH$`1gu-xrXGZOjn z7=@U)CFx+?tm_lgg*4x^Pn_^BoI6~l$(i)rT||bila{5bUKGeDjq<;!XgMGZoL(9L zc~&(q6M%m9;Q1|o@{@iR>$p3)UfXGIV7Bd54WI>#{y-4j6Fw8Gn7U~7yJAyGN8m^@3mi@{8|cR3&h ziVn)sZa%ei6@@YKnVf~`|E&GFo>|Ro)6d#_TseVs?b)GG^8t<6w2}UXG?~ zJzY2Rgv|7(lZhRkJP~S1>}2$fbJS>eT}??B)BtuaE7eovRDENT8&?=ex4?CGv^_vc z%c!v)#(!srlGcC%T=llZ$B=mfG)x+c$dQS-jguEmg&y>}a`8@ifJLksfks-Qn_@l= z^>XoY|5sE3b32v4j{i8c!)U?9s#Dc}0%q1SZnra5_WekB3bWj1nJ~TT3X!-QA2Z5r%A`IVvM*W@aYUd8YfI^0ziNQz!!-@zc-!UN4`0x&wIR1%?>7 zumL|uF(hAf`IZ@||0cC)EYaiU~rC;cG(UOfl> zP;pcb{xu@QA@t_()qX8tTT7V}rEj+ncI1BVd){7TOf5Owb;5>(F^p`@K6|eIx(SX3!2CvD!81{U9Qtk9J76Cw;RbwM+!={mE%7GjNhO?CNB*DNsl= z8y)|k#S@X?ek}W6A#@_dENKqPthx#~L?gdHbNR)b(-EGqj3t(Mg=`29s6 zX|&1kJCQ^rarT@7a*Zgd#6#C@cQ>fyFY^wE;NHBA({NY~1s2~`UXf_azR4RTWiKF{ z&c$FP$zRPHsBfqz;{H$^*!hHuKY|l<>F+M9?ROspoRTi{Kah`&sBFY35+)L1YmWtQIhSv6CyF+NS$f28F7$XOXIC?*KhC)V}6~IXacX$Y6}@8Pm5Qe1<*5^&D`@UUoqim6$KM& z4N~Q14%#8fMmaZ0uS2Yz_O{}#5k&|Ve{JncLB%WYuyKxO=D1jnMvw*SJozN$lbx*w z`^~(sR`Z&@sP+eR@}i&J&>dFe#K4L2K4o;f)9dBfB3L9HS0J_B2kp`dKDn2-foAqW z9(5gFq$f3{$ui&@VV`|t&eqG7VQtlI7%;+L<>pEwIzDl=si%t5!W)S_yene+8Sk}x!BR#Xf#EgB2Cri{a7!Nd z%S2KNu1M}qfo{Dd!ZL1_q+q5Nabd_rJt@|P9plcOPxKC7-*`&=nn70rkg)=RPmr_< zlovCGty}xA&&Ny0%I~Uzyd#GLXE{X8vj89LlfPZiF4mUh7f!4{%ZFO>fqw5!IrkN+ z!tYYPSRsD8y!aZ9N+TfP@MM^GwjTm82!MKQdtEV2K9yFvOQJ|vYNF(=X9%#hg| zr{%P+5Telsm=}UhT3AtTgWRj z5}7O_5L}^74xpwVwoN(n3LUOu2iqD6YMLt)hr!HN)ho;1>E8GYUP+DzJp4(v;^4s2 z5qc==aUfgS#n-rz&*CV_D&uKT8|kb#cI5#xy{v!N_+P7Xdp zz3xMr_$z4m5zaZtV7y^sP3I^)AG1kk3tIYu%x4vuMG~$Xq3()f6-qXcQD?Vq2&Oqq zMXhaRC-h$8l7t16M(jo5aorS@zSmxF#>j%x>jaxAW9fWlD0R+l9gSP=d>6qOU*@2;F4`_@*_YIDa#pDOq!F#J~!yU}s>ELtJ@OQZNy!zSe zt}t28UVl`7$`k8WGhg>3G1@p_i*aLr#v+rt<1P6-6>24Pa&mlL4-Re4>zUANwlzfF zL{N;Sa>?yZmdJ&h&(+%&xN{^~!izzdy^q$E#83_#+BudPZaW2EJl`5s{#kVW=zkVH zl%=*u)FK9od7wGP{N}o~KhAzC<`_~c*A;n>J-D{Pyhm^P)juoW=0%V3&VFKWZv)6=Ssy6zJ*| zsu3Nk&=J;9p9>9K&3qB#k``2&#D7Ln)N1=HiR@OnT94CECOoy?bor)d(|wOx&~3W{ zId-e}@X>17)I5Ep==rnqhNedcrM|vvW!=`no;WJ!FDO(<`l`;7HV6jDuB5HD+O~_W zF~JB?O(9`8HrQQ&HH>cYx&4My52i==MwV2z5yGRZyV`8xZtl@k33R@^wQxC%a)Q|q z#_V&2Lt2?aYTe=(yYy3F$)NF@uIDQZ=$;zWOx(;F&tPp|=~qOgR~mZh!dt80Kc@-C z_v#CKOn}S4G2@@p-)4+wrg(yFTLkE`;S%+a}W{B$){(_(ije?2Ag^VyG{fkwXV zq>N+nha=L<8n8iiz7yFV0% z{*<7asnF%Lh!gLZ9DIe$wAsq>5^LU#GU~CyV z*qOj`##)|tqGDR@I*U}GdyAuRhW4dVG1P8xb7WooDPNmWV_|VECH^*o&tlR~qX#K% zr1lzuZhz6>a+)aEsnAN7;%jIb^cLCLA7JSbZ&fv<1$jqXqBQ|wwXs|8<-eM4eb%g!@ystRkne0fRrK889N$q$E10lQh zk8dec^GkHIIX`Mud$J!`3@>mZi-ngbEXwjql9??$-D%N|@ZrPkR~D}dd_f=D0b6vy zObpA;`(0=oD2pioM{NFz3d_;`0Sn^%S*A|;XmJH>LJ?E<37@OXz=#CYMHxgZv=t{H?f`n}dbYBi zCAFO&Cw8uW2F|$Pi(=5dkcp(5tamDLYq*TQOi82q6}k&vuq+A~`qwTY`Mus`H!|aHEa!R6@BI#~ zmx&h-+yytOEJHzjr+fR?D|AB3lKy$oU2+3Cn#G`JrBR))aavQQ{Qb zJ^9|Li2ZFBGmn43HJ$sX;Pa2p7mBWj-q&lrizvm?o5j00HqIt_&+C-M?n0LIVJl6C z`vuCFSq?#M=9#$jK^#`K#&H~VNb}W0Y#^Mux@f#rS$oTOyZzmg7giB@8|18yuhL4Z z{AoqI%FT=t5ZKa4t&OMwx*>MgiSXwhRNCAfuMJe1jxeD3e&y8vkja=t3b&;53;oEb zFpQ-ly_6b8H=o+}0`v3pb`z?=+*veOrXpK<)<4%m5V;Q#& zy)c`;rydK)J|N^LBW!@+b^^G3Lr!!tWx>7gytRHK)92fI+P5fc?<*4fw_@msZ)+Yw zz7fK4wPCECH~N_%F6epEWfLdeB0p0Kz+87wV|Yp*Y$`hNp3) z7e$srF9xI|s9*i*Ak3}FbzJn(eZ&S&i^f%+K#T03M8Mwd{0vU9n`I)E0qICu4wh6K ziNZZ}jKjzEq8{;YL>Ipfc6MSC(1FGc3c4rBQ#rlp@0pGunt7pO6vwkT1p#`ZN8~^r zigCw!vk{f-clt?1%(!a#5QZf99%rh$*5FS8LmJhxVv{N=UOUoFPo z!+tsyc&(2l>99u%ZM?!fmb3n(_y(c%QUW zY3D`~939URbkB1Zyb2Y97G*AUGDHMtPL?8s`F973NAXB#g^-hV12J5v3x=y6rHkkj zXhop=sus9Al5<+C^|)q!T@;bVI3#9}bjhqs_hUNFN`!?pvC1_4|KJLFmE#8T$#wT2%=dGL|FdTkdh+{=z9pRTb8<<4q zvG9v~GS)MxwD*8It@>+iA+WS`KPH}+^Yz)0(f01B@JBI;`w;=!e}Py07ntBMxavm4 z8taKQ3cM7be?a*m-*`Ce{#+Xu)yDRXqW2|ZCDYJ=;laKq%z^}QgrHV^v(CsP@nw}>I3P4kXJuoy?$TNsnwyn zMq1~!NV<9+)U7>a@pocKMUPv%;kdSr(NVP9(*-0Z zIwuAvDL|k?QuuQVxGq!!(oZChnO1n7dO2FaM{qD}G1>EAP?J+&zP^2)r7)NK{N3F^ z*;Ft1_ROB@x2Q;4 z=e)Auxbwtm>_G~@ladeW+@cr?$m4lgC@d_zjs~}NcHZ@%-*ObS?e8bY7tp(wJ?ipG z>Xj3i*zM8x0S%M1xLz&e{zPuYwpx)Dbe6kDJf}8mUqaE2I*~(ORa0R9alcT^*~c8N-y!e=1ST6ptBAjoJ2=dGO(a0N4uw*wR8xg# zR{#!D7&I06flKXt^Q~cTl*I^z+yRD*H8|zpOu4dso7{``vC=+YgrxSvYuKFAAJ-t7 zoI}6nv$ca7ZCfnoHSgk9*HnzROgOLHwa)_c&#E?2X_up~>}Gd&@3xn6WEt0dO@?9- zp4YfnV;Si^X>uI?BrHxZV^7UjHD`&8w2B|XYFS-g=iM)UEw=T;ckMTck^|AO-_j#} zigo8X+dz^mI*14=2kCJ>BcpQ^0!$2zd3K$cVcSLFQ=-K_=eAo?xtR{`Ez&t-bv+8z zWyd39w;_7yZyG7|aKYlV;i-Niw_$+$S&=S5Xm8&0FeP;AU3pD+071_HwMA52!o^xo z?D|S57cGy0;6_Mb_R1pca7vjDwIwqT*jHB-vzTN@<@S=*kq&%+rGg(Bs%5*NjP;ZI z%{%8HisXYvLk?@Ka z;)#DX2sYU1e4a$49$$)VR)!yY`-m}G+^C%1hDUZ|QPIUot!@xJc{q@P@?N@yp+pump0`dL)Bq$2df>F7j+Or2ZkNiQeg}Beybgw=>!dBB* z-D3O0%i2d9`hMj!xhnyA^qu*(400^p8=NM*FQcnpH}4cH$JO$s9J6`W3)TO;^}hn7 z%FYH-1UC6Q0gKZS_a%7VpJYBQSEjY^OJB4oZIHH6cR}wA4vltd$C9@o{yevp3k1jU z!1eohv2Cr3Ei68z8>r6Jb?EFVSi+H3tO`6~*$zms_B6RIXf!qo!a{!UI+HPhCA z)F3@bVp@|WHo{`6>ne1Qfl1wHE#7bC{)yi5es{Ds&pSF~fU1wRV?)SB8f)UU*Tf1A z#j{v88%)|2u*6gveN8nnBwTS;DxxT4NnkI(P}(Sray>r`Oh)t?Q!Wxs9hg*|xY+r4 z=!T^hv+8oG-{LyHaAxS9sqGqVEhKBXX z1aLx8hZcAW%zruguIad7PHQ@jk#)%xX&URxIRSxYBk=3CC)xA0r`W_GttdR*nd?+; zE4vxx=`SXT7F84QezJ}$N>?w%9qKY>5S_hq_c;QQUiP<}k&JJ{w6~0(FATfvp>^*Ki{&kOpOh(y2hodh**tHfqz&z z`r=|B3!3uy3R3`k5pYG-A4vkQcq?h*E4F#aq93=L=bnPok}Nb&@2pxsqJ?$NVjOs@ z8pfef3tHrS?7nBC<4^#&m2B(flEbogsjOn@%Xn-Rcvr%>nOmDM46@=d6P?3dlKU;Z zAgBefJNIo=@L2?#{di5mou#mF0kU_zUwQz3>`%&8+moB{UQVzEf8~mib-e6rgEn1W zy@FcFETS zOOh^TOi67146A~MXucm${VaT6bKz>xc>^HNTc(i96SO~l*F3xAS@QJRYS*3~fy*Ea zUD^r!;gRgC!57g9@Rd&cv*QlPWZj}cE%PW042jgarExvu8F>U!Q8=`Z>>$pxPl^MTR=kW<~>HOm4&J@bNFIY_(b zBI~=YI%zHt1pZj6eNE)reu|}%*Z-XugXIuY=x)WzrfRvo0J%hsL;O;bzR1M*#3*^< zYwG4y?+^Iw@?u+$BZH#g2>;;v#iER(XLi?jGOonk`uPVshL%vwcIxZOJhep0r&D*c zq_1lo58J|HKr|s6@c}fWWIj2h+|1q*5BpOEM@x!t5nP*i!T$IRW4ON$E;9Il7paQ0 zD-DJ+WvO>(Fe4NC+c{4-b9*#-IZW{UE6nx|rL|53IaCAx9Ab<}e2G51FI_Ip_^Dmj z-d|lQ0e1o8?Mm{M(niODqRu{gMJZ~5f4Bsy1oyy!a4}+4zHaVctk%?Ler98NJ%M63 z`Yh&>uOXMqf%m~7AT%Qjt*Rh*-h z))wVFGgPb**yywQPMyZ^Un`#1n1Kp2N`y3ZWpm}+DAf2)pgFz*vrhLjqt?0`$jd~a z?XUKp`!=DNF%Oi~83GmHiy_Zqy%_ZxO{;9zI7Ta>puX_uBW9{ojv$jV1QN(qY&f^T z=?c9L<^xNwqsX}ui*$@%KTk9yvd0#eX;lHf2-uqIR4seNnkG{fWWHKty+Y(kqc<%> zPP)rx-XDbhh!G>x`D};g_zh`@7L9bIb;3n6C<6S$^ldyHY6;iX#`O&D>}S=%OZz@# zHS11;k)_q4u=&#O6*4q>O(Kx@{5f=kYxkG2YHp33!?3)XXH*pUx;Grjgq z8?`4zwO%`>Gw*OoQ?&W5jcA=kW84`jV%)@V&hvWzRh2&;dET+F=o{nyXBWd0ZW|-T zCu&3LtThdvF7E5*5((U%Frv+*j_A7n@d6k^JZUg#spkM1vs~N}`!eSMAo<;a6;z6s z+0RjDeU7k;UhCBu3vH6dJh^{X-ut=4(xL1St-DWyCy~-q1VGU*S)$?9gx;ZcPY!o9 zd}^Le08k@Om~>nWpe9^2KfITJx+s1tyt%BB{>HuCX2S^gyXQe~@_7-%q;{W6JD7~0 zpEg4=%%cIq)il*jwqKq&TS{uX$3+wDi>Z<)K|Ymh-LsMz1ots&KF}u|ardDmVNtNl zYQRdSj0R9m;I2(4OZm#>1qS8jzg)F>ZoV89Gt?sT0STN5`t=bnuPuie-q2#;LWB9%y z80T-GD=(vSoALrtk#Q|NcT>ovf%O3tj(=bPZ6-A5>Tt0-;}bi=Jk}y|D$qsYNcVg+ z+wlKNSN@1gEqF#hlg)OLn>TKGm@qxX7_^W6Y&_yTo_~-vF{U>3R;ul&XSvSwR769l z#s8WlPr9dH{Q0Mxysi5-Hv~8y{7<^tf9=+$hlKaQ7HIi6>C75mItI4>D5)dhB3l3! z>7c#Vo0`BmEX39DFApi+I2DJfJjc4OTJd^o5Or_xzCyJWkw1nITOCPM>T4ppbz>6O ztv8hb{bzaCmeUx%S=SbAQQKKKm@FAQ+6$)}Nl3O3N=m-Ar>K^-->;5mhPb}ZR5`9L z(towRQaL`!D8c40aE9Qwa$n05Ur#bgpBdfPSN7GGg6H>YRtMBBmu9xj#ZT; z{%d6GeT;1!na;_T=pgRu+pb2kqp2^HDx*%D19C$WUY+bvNy_ChVxslIwh!x<^ZMp0 z+(+H}18`-2xF|dU_3B(4>TH!c>AmJB`yKDJ?qb`Q-{PT8zCh)pdlTq2b*gJnT3?>v z!z7YBHV>bK7ulRaEZcqJeBT~E7NYJ6vAIcvVFm=}YMX8@XW@8tkQf>fHj}J>R`&4X z!XY-r@m`I{VBd7NY?V(@Ycl;hn!BOg)l1xtHvhB zv&~4g9%pFP=zesoT`ubuBl<4JV}2vB$A(sZmtTp(QY)5r+a+2=&>1a zvV?7ZHovJ!>krqk*A_;@Qs#M9q;mIm6)2BDH}J~0i4p(jskx&Uy^JK?N%CHSWG6PID_ z|G*mF%koS9V?=TH8BK`Hv2!bB*AY^$uIW$wo1k1;O%~Twwx4{93=Ws>aoRp~;h^Tz zib!#F`Pzw%TOA?}D!bdZ`q`3q|4{sWWVJ@wA=1KsW@Eb{F?Qxx7x@ zbx~WlO_zy;$bbU@^yMo4CyFfYP1EtqIL?f-<2t#vOx{jz^ts$il%_yau7j)?z!Npq zSA%D&1@EwSdIvM_wQd{eJyCCV33Fc-Bk#StF)#Ugb)Bz5G;8_$!_kP~VcU_NPbn+9 zSUH9#%4YzSBOoJBuND`Xil9^@5J5Uf^A5$;Zq4quq65jVaxU4Wy+o~awLQ&87?&Zcu?!f?7qm}OA(R}Bv zi{=LoeQ}Vj+(ROztx2xPZO`r9-mm$mnLQbzpVlnrCjzH(mE-^Qx&Il_M!UMvSD``} zhY&g1-J!(iXN%LPcWNNbDkxL^IgE)~d?k$ZH(!tEJErj{Uxyyp&g!KYjdkJCE@jC% z)38iM$JWlSpclA!P?nNYBfO89jTS>vDK2)3K0P_^OaE?BrolxeuoG)@ z{e+Bza$U`U?#L$??bYDOHmxM+*f{67$?UooZE&zP#Tw5gYHlDk0;Q{;c8Gf127T~H zlC!oJs}zqWnv3x5?8EDtLCr`#utHLRtOqq%Y_`$-gY*o3JJb_hB68uWndYDvt2 z6Q0GT!(T$f02G}o3M4fx21iKp(g|b%Yqy!4J0kdJ0{Sv^sh?07-@MO-aXr17eBNk+ zoZbqR{WAEMa_NP=^W5fF7~@)iW8b%#`0HIWfDL@9NcnvCJBd^EaPR!oD-Z9-c_Wc@ zjqfAUZGUpwOnWR17X65xZ#eLTF~&5f{xIEUGIWkj=m%8(h%Qf2BYyUiGBdo-qT;}J z?^M$;t~Byg9hJJ8eXyHFBQe}YswsE*h)lA{!k_C+s9NXrZVunOd+(@0z|3ZCsS`rd z0o%mKgr1Kl;kG6U8U>a*PN|b!_^I9d$=9)_>u0~6Mt9Bn_+NaAz^FAATn&G-egit- zM2K0GQiJ!tCA~4pXN`P+^5<*RbUcc=h? zO{r(?4UI|;#vtF`d2(f8d*l36Pn-)QHtcf@#J6I=>Pn0pXEYvH^C!C7M&Lwt=gI2@ z)PZv!dd&A06hFdZF<|TjmJIGSaKx?-;k8={ z_;cZ0G#Sb`KsY zz+SC=F3~{j&Ag&(+!vXtx3{-$T2>Ca^2KtYe?}*E$1i9*sf+>?ie-GG_ol|wP~e5- zv`gJ0UV<|;@5oXjD0>=h7hl5e34bLi7Ja(taOm1>Y%%IIFhJV!OC%{AYn)c{xXq{O z(Z|7d2gtC2hu|k4Q%dq#3Y*^#IV1Y_{2Cs$Mzvuu4$sVRKA;m>pm|1lj?$?yZvxDf zYt>qB+rn$noB>X4H~FzZW5r~VUmnJk+ys^bc7rm`aYL!sLAzy70N1-%0@w@JtqneL zm-sI*FzYLjXpxMfqTu?|hO3Q~^_`+JCwO(SPK#zbno`$penRoaxZV(5`lE@h&F}v- z?sckoxP%^ z6&ywDBz>*So1&Y(xvc@RxEn--I_Dwu&qr753x?u=1ENleQA@nL&-FBa*6^i`AJ5d6 zA&WiuGq#FHz^VlOEGw_I?b3#spOsx(3}|EVETqXH$jbn66|Y{xiQDIt)Q{?CRwXEd z=e+dWR?NBLz=rC1wlh@OXhnlHRr(K~w;N7|pqROjWHI7|-VqV}Nuo`1k*U4RPxpl& z-`)2}PS)`Q;HYLRiB zD1AGN4UW~#*Amky8RFMDmrt3?y^awp42|bsCC=BXSjx;yOKag#tuvdk@ab-Cgk9;6 zokl*FR3^&NO{EYScykvu+h@_5)MPB?7(@k<#GRu}MP zH~-9!_0mEC99Q`Iu9Uzf$ z2Yc=QFfrZ~`Exscf_SU1wG7#c42ECV73fw|^GRs`?5W)@eGYgJfAv;lYPWeoX~So^ zl<(z9mRlbGv8wzydR3Xj_SMm05Fl)ETQkF*w-`?~w?j^;Ic4EF#OIII_uez{4ajAd zU501R`)zNWeH?l*EQV>n>C|H)2L05EjDxM6!}-sKG}20Bwy@3?&JF}C@2X%E+uj3z zs3~nEUM`+%oK|knqUS(_YCVbn5CgriH6@n3dP3LZs3OrHbtio}BuG8~vT*%<`rGBN z!XDV9Kk=lTug#6uTPxhx(=)oOqhr^N>?6&mhjALS<$ZkdjUu#bhb!dKJBaFNUQruC zjzVWU-s00%Z&l%bCkE;-H8mn()HZs9-(iG4@25Fma>@oc9co!UwCZrE**BBHlzK9} z8{2%C*kiYlK-O~vQz9!yT02c&Xc(@a=&HZG-g}cbqdg+_5BcjKi*5Y3nE3-9!PEfj zyL$qq$v98%wx6+lwa?F1e{#E2?Uf^(Rrzvp9?nSDeAfJ&D_N2qCc>>d^c)jQ8hNKr zauGWYbZux_Vmm)Eu0%UcdaBuDgO8Z1m^2{ z|Mls$qIpXUE#Z$P2RUr}zz-Ltzv9_xHId(h+ai9HsmeVd{eVK@XgMAjPE&n_5&6^# z+ge%syE5e!IFaHP)v#?cxQ~6W(igu8FxO>@iFF8i$-RrW3R0pHD41>Ono}e{c4Tor4Q9wz&(F>IbOnKiv;iFyu`Qw z(p2@OC(((un5vg`hc6z6K#hTxZQ>G599X2@Ef1mA#`&}_P3Zw|yr@67=@H9B3l|Po$71TKlt-n z5F_)B^1bl(FWZ#j9`UFqTLRE|VT27M-F8EyDc z^mYW#bxUm6@o?ujc{mk}VfrP|Nf_L;zS`HyYEdW30n*`hpv6X_9&pB&&0Uko&swNr zWS&U-xhPKIyRQC9=*t{S_b0|7w!y|pPB+yo058T1%9L{pyLX}Pt$yGYm=}QSD4?>; z6zW6YJ3Q~1C5__Jc^yesglxigbz%HjuP)T~@t&AU#gDeVfta?7IJF4-<>&yGZtCxm ztLM?O{B*VN7vpv$wen>3UX!#wX4Zh@}A^0C4t;X ziL~T76=c2vFTN>2tzed*ns)q$`kn6rrtQ_B<3%6K1vnv$irb~`Xs+HKEulUy>!AbA zFKrxD7Y9X#e$h={5Y$*`K8!og(in=oe9hqP*Cswf&rNu7JCV73V%})laKu!5TV=hU z<}tpoy~3}W;9YX(<#IH~8X6u--oJXaT*}~QBYwzG|A3qVBSu&+i$8+H64g@?ORSO4 z2%)WZHKe=TRy1{BW;>&s&3({Cjf-!MNkd~**;GmlJ6a)$X|PvrH{5 zngto`!r@FEDc7}w&Ns^+wA96kz{cdl6N14T2`rPg4P&I^Kkm#tV zCq3#}DrF!GD<{{>Cs6K3JCXBI;zhV&6l2fd{!7~Y%ii2!IqVgLeFy{~_8+iuMZ=t2 zqH8=s1eUm;nKi}LYk*sCqZd|Z)faF@yiExix86Qp%@UfH{N~$=S#cG%E5>fZE+V_o zcy00Ri%f#OK$LE+6DSrjcgJ=$;tO@u>aV}jv({T8>{p!QX9q2W8(fJ45+q$MYjzTb zH5EVu78XW|LyohhEz9#Vb2W~DtjK%QUA3Iy#40|km`}-QHR%)K%p2lMTv3TN_nfh9 zFnRTVwB#;+p(Vu`n{6xAzJQ4D_3}oH4eA@}#2oQ<(Y5?_75t9}@edem12WEGF?eb^ zoX#MR*Ts}kW?OB;U0-bs2$gMrF)7@fOaCOKY#+a06KYJyeaPJY zgg_HBFX`MFbDej%oSIH`%(&w=Rl)tUH~|3mwt3P5FejsJ!GRE?Z-kcJN6po-R*Sz@ zLbFz%$O7aAOSuKYS!9}2-NlF z`clDlZ&Ksz&2RzM=Dj2c*e=N(2#i}iU8AwKe#Yf9mK)_12&^MatIue*tpgt3%ZPZa zODRjA8){4)h&0P9EVSGb9@i?FyJ=GAdZfl_EoXg4A(K|*EQ%xwsPS!SlKx%xcu1=% zRcG3t(!4T(ie4&^$GNt90%lyK zgSu#gD^IWLT{V8GKw1x{g&GU%&zU!nwR4k$vw6c-N`v`FuFhMG_>O}t`*zjM`pbRa zmg@xvBe-6CBD!~>QTX}9AtS?0D0KEW!Ylz6DDt#3dni9X{@jg-&1`j(6~sLA>9J(Wke<6D@%l|j$Uvn_9NZR6VIwisS= zrW9`us7+F^{XLNX@$iEVdm}Q5iEd$4K1s~H_G|M-Edh$~Jj5WTK0#K}?WO}cn^W>9*AeRE=meJ=3-+I`jtWhn z*a>)g3?1iP_|2iWZXYelMxVaQMS^7BKFb4n4R%ABumf?oT$+gqR347q-gV=)!qb(em+cB&zIk6#V2dcfRs$_!*#4iyLeH5*NWYw$3%h!?C-SUKM(vvTxt>5 zVCvUqbTfBHEo;X$Q{8D=v=56Oyr10`V77U8T)``C;u>B=Hqb)<<@og{1n1+0&r1?1 zbNNNk=cc-cOH8993(MBL?9>84vMsWgcwK|>4Ti?O^@H3z#=l=uz_~RWg&MzUUGUG0 zw@9JYKCD#Emhq%18Bu}!qMSrl?((#A^%4j0%W{W{_ga#A@(1OB?DP(ve$^AXF3uPd z6S@L}t0Nu>*e`V66T_#H4bxwZooA;R6IvV~I#6GSK$pM!>5~tR`V;Cc0kgc&JDCC1^U`flXB}OhQPq)-9orR_uRgyptRYRf_ zx7jbdDNg^fWBzd@?+6tS+<8@&WS=VHeC%m=wPUeXn19PmWCdwffB3l4(%(>VsK7i( zTQNSEIDV=gG(tV7F!tGEzlwY?jY!veHpWb{bTAxOy79W=z}x)-xqe`Ce>t>)u+P1! z{CB=K^s`Y{S9p8x5d$zF$> zTy_afD?S}9S6E7T=fQ~=KHv?fi`A5ejqgzE$LBR zsS*{9+5hb4fL1|03HiF{reE=lFqMBI`K$wiysEFLESDV-ZnR%z*u60j@&EW{o+eO2)NOn#S?&61neng4yZNIEhCgoo2*69}-xzD6ur+~cm z4j{jLuCcwmfg-B)FJ3+uwGy zKgXoOGl`FbE~K0<0gu7q4?-m+_Am9vJvf<%gw=Y%EEvi65tuwd@kL1 ze|Eo~lFr!B*1VaNM*~bHSKI7^r}mdS{GU~g24Lf2bqm7>k7~ep3-rD{LUl{NV>Z=0t18kgm>yRVmVM@a|8#@z|-5=eq3xI=&d z!2${H5Zv9}gN0xR2_D?#;O_hMB>ZNeOtfZlrk=pNdNU-*ImKdu{ertqGxpIX(hsw0Ej)KW-wr3 zZa}TC&a`~gI3rqlOiE$jwAM~T&;NA=Cl4L zOgzhs`5?2J$XA0jmbXO-UOz`kqDTyz`I+B!@xQj24;Isiqx}__{f%JI5z`*GLFqP9 zIr6Tqo4!4SH$wA-C&%UEn@@D_>SvAD12mCuvkTf@lZg5+j{R4E^gm^+KRbc*FkzZ% zX_HMg?^%8^d*@mOL>Ra1yQEeth=* zVk4uabWu9CI2LlXqS(LC87mN`2X(*JH^c>ADl1zrAn8OLD;!{)UV1|&P`s^|^aN0G-!?bDnI|+VMeU}r1=!qW>9r{a+r{MDl5(GNZJBP?Ih=nh5 zzpEhY*{TPh#I5G!<$1o1BYNiI^W%P}?~eWx(w>rDm)Mjo*Y`oFD6ObDv~iLLn$XRP zF1?fN_Qy<;!p3}Y06DWp1(YPM3E$#^>=NvWt3*E@ck3{@n(mj)Mwzmhb>N|9WY^5& zMTcu+F={mooPUX@`*$(G*9+Gqo}Ab$LlxEPA)UWSbZ+nd>;1=_(eBrc-`zg95uAUF zQt?U7+s~;V>Z>vebY;id+0sVBlnDAcM5Dh%`Mpnpd@@kBKO3^4s@YvsvgYOyWF215 zYe@6gd;MRJHDUEtcNkD@i0J;F8}p%}q~_BJ->gm7#3FwLLA}jHr@*;=?SWp2{UvUX zdGL~N$g56lPP5QMev`z8^%ZS>|1Z?;?+-RILw_;Jaaic)s6Wk?bJ`#|{C7U+udMj3 zFaOZHQZ)2ahXKWTv0kR)u!abamtZ;clJp}p*j&V5CLHUo+;2L1hgN8`0gQ1ev3X)N z*`yvylQBxyQ|)QJ2YpiaE}+?4BTBGfA*4{f!6-aV@UJ)SABKa#;%hgj4_;uGPJX>t zX{ek)5DXTCII7WG)9V+Tn}I{d*M&)zhx=o4qQwrCztEuP*Do@Gt?W&=>yHxvLtZRg zWGYd4cRf;EGHpu2DnXf%GjjbU9iyu4f>O^~!qu#6DWQz(!_CQck{RUow8QQ1SKN11 zytS$;Hnjk$sF5_{Q;R_>DdK670maao(L(h5cQWq2u->4%fZHbN7}T-1@bp1O4joKZIJrE1NeWv=~v%; zcC$8b3&ocQZ!5sS+~333E7lsfYgQq-B}>$7;}}!_z_BfP64S9G9gOAS;MtxJT`&C< z2qyANSg%BkBSIydQTYeiL!IO~VJsa5*#*MJl26S&l_`>+_B3ME=aPR_btjxF z>6DQDPYUG!-^Y^aIKGcN$y4~{WK#sne!)~lQ5SL?yO8R@VP63_7fZC+X}`&CbzY12 zuxyO4n1MZnYcIIk=S-SgP0K{Etzu8}^cP*2*Uq z#<+U-h0YWImRqI6V)i2&9vsRj^ zCL1?&tTqm1BJ&RqH$nO`hI@8C-1trgtm$e=Syn{!^h8gGwN0`p(77(U(ti%*|{W-McZH z8o`!BI-Dilbn|qBS2dnHVSjVhKU;rnSy>&*SCrGqmz)6Svg(3`V(q%ExS`8g!G^ZhQtNbdJ{2!v{Ayqu+*ocTH*aPtVp^n5G(59OB2eQUpGY27^I@a@t9J*)NribNIBvPFv>P;eWa!CziYuf76AU?8r z)7hD1ezLWpmFgwAW`DSE-+zA|m4-h*?~}S4-Wv1<1E{orJKVq8B!ozC-=~GUpY%R+GMKwzw-OHPvOO`>V(Q`;+-1 z;Ot1Z1SFv@PO{H>xSP}s<4g&L;?ScdlcyX!t`f*(MFh4Rn(wQA=*;)S=~IS4VZASd zcJYz>sN%Eu2IQVeo{c*L&53&eN9B~d`eEGZ>!5jm(vcbQh2gx4Cx_>#kqu7m<82H2 zu&@2ZpW}=Feg6r|*Yj$zG(8o;iVG`!|2NjYWx~toY3G7L(~@0fx}=Wbu~7$i__-34 zYH$V75PL+CQ2IvZphP@cg_O8RKCmeK3+zMmcL<-tn^cv^`!=F?EGI}AxqU=lwBxQW z5i0PE^&!7+nQb0~HNY_u^xYP`8r^bk|7(u5q%IcjOqt#QCAp_TP%2I?$$G`Ic1kyK zSiSZz2FI+;pcL_bxAZS-m9-iQ+jYCkQJ*XY>c|?%;Z?7>^JslF1f^}g|9xd66$*VI z-a6FQK9;fgN%k=6Mux7H<1t};u)stTrx5=1T#>vfcy@8cX&dwDuge<&0i0^J~(qIxhAgKwB6SsHghZlguHkn z)w>#erticvS(2xCXJ@G@_xBr77^WlI$FdS;ak6UqlY#8mObb8nyS4i5tw8M~SnX&o zL)DmQj04Lscgs-S!yLqP!!5|b1>UzBQ9K{N5c1SZ2@1lbl=O7$|4nND z#R9V;A!j>``gK!WhT?7cs_#3f70UN~Pi^|K&#s<(VHT|+OD5Gq*fP*T6DfIv4zD}& zf>50fWH5|TV)qKW8l7BNH?a=ZF!oelGSnmz>j|pcQ@0qnF%O8cYgYS*|C99` zIo2fq0c6+YW(d~erkl9ocUYtpd(VR}#b-HF5RG}E%U)ie?yArikGv(fGL;A$>5qis zvqcS{qMQ(=gLi!NZUp_1w*`Y$_^3i4tS!e{F#Kj_5FGi<&($;WXJbedn;w-Z|7!>M ze@{bQ8{Aj92Fh%O>T^k!LvFwN!GhNT)m+|rl@+7IyA8gttgHs{%XfE~AtW^$)h=y~ zd`Y=)<}hTky&LXOd=Px+KXI2g2n^rh?P_y#fsD-F%E>KgqY7B?!5hU<1k>Z=&w6+< z#Q%2);m^VRzrPoLLJ|&R%ym^>uZss)&*a<&dF)T9l(xo?%R9bN@VO5cy_glluIX3x zzL>N5lENMd`6ly%1>}6aOEh!5;Uk`G)du5Aash<_UivT|-EL*s{}byR>d^_~dnp}A zr*ddZK|5dG+}O4l_~RNB;r|q-3HkCf*-;LntoM^7$ElG$Jl`&sW?Mu2c+k!JbmsuZpDNlKneM>k~b>m$f9zU z#F=8LDBSDYIS3=VyhC@%N7jqnXvei10+-g9NB)>XW>Py&#yGNU{hTQST9U|NFzv zw@o~qSb&X>rph#kJTbh>yk1I>52kcq|8*5^3mb2lS}^ZX>-YYLUXL$#zHFS9yn-o5QyTwU;oB5G* z7Mmd7S6ye8u)cKK_yP|(Z=Hy0d3Wbzr5rS8-fc2Fzkl^Est{EMsA)SpICK)bBnwha z%7MF5bpu{dc4(k~8i1~qX$^fVMDFqfGZ`eSYP9>&(daYtq5Qm^;6Lto2V~HUrfJaR z%onre5MB~dc}k;Xa^h}gNT(XET^2V`m&B)!NIoR#vb4nN(<`jD3>=suj7ROdTF}#i z4GTgFfxZfCg<<6`f>YZN(9MR3fy8%Ul?4Psi)Se@Anm8kdlleWkC!cK8e9Kh>yo-< zSgb(BE}Tyu)6q~nUMZ}q+g#B)dydz0TIK=8-Z~?>-v!bLZv*7-SyJHqPlrzf!*fTL z>mkB2&Yj18zpi`TO(c!wLqd0)#^&Z9yaZgGX0`W+G;}V&#(aPO)}e9o;)K~8rN-4d z-X?aVIC37$JIvKC6*FV1yP0ym_USI9GRNO?T*k_+rjPh0T`j|kh%dR0(G0s0Xp>wY z{3Djf{nPl9=(W@70j=84o4&BZ@;>tfl_?z^POEE-aaYgs@V|FfBy4{6cpO$^Dna0BL4= z`_xYoQXdCP8Xw7AxsRh$bL`Y$LE=?Z!GnlsS+0Jehre)d|1Dz(^T1>X63u-bV4^62 z3vPRqSmkUJiJ&i}Wi;ZIQ=AvMsA0Y(zQH&1w2b<~PAXn4puKc-3+sX#Rq>`d)4qs{FMe~SY~9Vp`G5m6raQzP5|dvXC% z#Td}z?~*yFO|l=5^8pHO=}FQO#H}lg{53_JZV{;&>lfXGBM36%H(9U@>y zL{&2EFidCWoC{t+njZM#EneCt>&+Ke)sr)bearE69tA-W>Gw9$)8?G5N~o9k(U3$#it8I zEXQ?eicJM~z?ffKUU{m=bmo2kvg%jo;II>5X@xyX$oOXMqwz%K!+;j7PHNQ!{eT={ zI7BS)a#(8X*Lmklgt$E%hMfQZ#s2Rn=}&O)Ju8*1tIZOtXA(#f`Zu&kF%VGas|1t< z?#MngE}84s$MZ4GDBW?Y!`+a2>FxKZ8cjNk8dv@RWSt5U{H3b0$#JjV*QQaP*2nX! z`yJY0CrdJ48U1EhnUwbnYGLTC{4;|Kq7>+U0iQfF_Yvn4)RY3lKeYu8LB;c)|S~!0P ztnVMOZ$LX4s!o+YHDqyU&NBQ{JTM{FdM+1cZni*cfD1Ob`hB}yq#Ef~8OXm6Z_hEj zQ?k+@uBxLWlp!={4Z)aG- z)vDID?LT()mnP@$EBsM-I3zjLYi=R&>z&9F2EB=dx|2z;?Rwce3cgJhL=p~R6gOt} zdEuHspVSv0z*Aq$zhMAHA}eWp8XV}5L%JCf7~v)h@^X8lw$M;HMST3ZS*dc~Gt(Pi z9OE9IPu*R%|FGOu@lfnTMJ0(tT;>5&@_9O$gVtH-3jB*~dxKMR>ZBS75t?KH_Nc=; ztUA}`D%f@689+QFJ3%?O2rG2QuDYNhIes)bx#bmFS1q3h?a&uGYIc^WiV^;0NfoiX zmmB5p4<}z`T$AdO#7UO;H3JhZW?~K~4 z&h8+COQ_6tE;^zmg%B~jmTpbU%m&P|ZYf)O;en6xs$wr5HGO=ncIVbl>FYUlB2GVR zlve#i-c2|NS8;3ntL|LIA8kH(*CEZceDc5|C0>FgEvTKu6g=u8-8pAc^IcMg%dw_d z>!tY&xcZOQ3!Zbk5rqAotV00Hr*2LdqiV_nO?K?Dj^uV5OJ#gvvg+~6BVjIqLF#@( zBf#3HNL+O~fx?^wT>h^6FD>BTKLi>Q%s6`OyxK4Ic&maL_p{Xq+)ozRWvirWM4O~m zk{GjgbLPK77W?;YkOH%TVLCou-Y*B2_TxXTzmUs!h)2Mpg)X46y|ST3GXil|!WGNU zhF0Ew=$!ISEw#|%crc#dxG^O9C-blJgUVK?pQfII79L@j#l6iX^RgI*K`G_>fS<(t zrkboG2}xU_Xx=spZOv`9pqk{H`|HMi!1Hk#SVfJ83+d|vG_4rFfh*(cwh6;>j zP(luxXgn{RGCVbvbTcO(2)LD;{-Id>3D)=1d{Ks4i|qbBmwN93FfSP}8nv4qdReH> zLOUlm>!iu(1bo;9$0aV}bpkXogJUGPc?}MRu znyCsN!Sg3{8PY!LO=QjQZeT>?xad~h&Qjg6B!cdWE9hSUf7=% zKSf=QH8(B7H}xf|gtNx4Q;O=j@S&x92n<~lIX~5f(zkQsM>QlPw!N{FZx^$df8+lC zy?WV!65GrS_EZhEmT6-#A?Dpb-U>7$2~S&0Bihp4=6tqZq&AEddBs)EU;egq3H;^3 z38jRs0{kn!VESxd#c~_ycZgdUOoM8QFEVu!UWY(jX8Z)!+O&C+Ao~SMHEGsIkxn&-|DMcA=H@Kl3tKH{Z*;J4mtG1i_=N^gThIoilhYc4$r7GJ~HEfdSt-;k3{4(H%mwAG`uHPTb&YZa}- zbv-tJRL>=xb)2?I>244IJgrQ7P*g5fUo1l|(rFQ~s$Koydc7*KG4WZ4fy^aDm~O?> z`L}pWbTGLa_vg>vP5=VQZh)z#|8x_xk7UWFAe>nIccWL5h16fgXsjtn0qyqON6=Yh zAKVJZ9kZbIEx0WXLTL;4O8TpEVBNboQ}D92TWUh)>B;oVwhRIVEY&4T3>BNaSB%Ls zO_reYr2ucmIRR}~|MMql7}>PVjT<0x*X!}cTw_@92wig+oYZW>c8)BpYn81^j-yd` zihR=}WQ7{G#;#n+rN|gXW)4$0Nj1avcZcd12}*gDq7p+(WwqjW$(T)nj-%#3d(rjy zkTPTyfALzeV3@3Ef2pA}GJw3dZX$Y^o_m2!(9*s@ZdCx6azNJhk&YyE6=f`JPZ*D! z+)DrG^W_IK>#Ms3G3|xW-Z|y|iMJC*D;2N3p`^>+V#YXbFpD3I42Z8@+0pP2mVPY?)ale4g`&o3H zea$;TqhwKb@?xIBodg9AnEuLlA&#M!A1jv8&dsb|ybUcZkXtckTz)vjG+eBAEE)PN z|MF3kUXKSX#{)pn-W)oUgP9VVkK5CFGH-4#_J1`r@Hz8=>>z(zAox!ANp4V1pmrYa0AdkJ(%zkdgu2&1bO!u zrNtV0hZ}ZW?!OA1v8dC1exirgLWadOBTkPyJDqHB5`!*mMP*|dFfAE>LVEg%VuUM^ zI*&HJoW5VioUJN8?T%>Lt?tQB?|QAiZaI29Zv+3eM`w*i3LyRpvYH^hTW9-qA@(jR zE|9lyzoS&2naqjyed2AEeJ;VkYDumHaM7AUiSH<_L7=SIs7r1ccEAbbq)GH>Ag9_{ z2A)>??=j&WbKD#Vp{j@4nWS!9E_r@$7sIgw&I-OB>$+h~uwpZ};Q_R70 z4JO#@!@IgPWw9BF)gKQwQ_b*S9o%sPt6fb~SZC^EqK!XgFuGS%-k`{umlQY&a_||! z{&8&h#$(kUR19n&3SIk#6TW@FbQ0;j<8Pe+{z~d!Jq}o|^Y4BbA^&IPn&WVIgy-KO z41oi90GzIXeD8V&lXj!3)4XL3_j0%x!>vU(|4z(*!j)EBlbpkuSKWPtN?Bb`1r5Md z;kbTf<;=jkO9a?Fbc)xY<+NA>+`AP`L!%>5;TdeTTiD#xX{BzHW%Q{j4Ulbe3FHZ| zm02j|==mO3jv7RRa1@rMvI60o(ZW!jq5)k^`ZpopRutw4y22_M$R-2B^22;bKim)N zO5xgmyu9nxc891bF6H@67Ebq8Piw2JS#^i+p5AX$Xe$oG;1Ht0J(s(-05E+{u zwh?}s&$)7Ozz}YKqwCp5URLA}WPnD=AE)!Mr*b+s-OOnfd=IF_%Kql38*^U6D|HF9 zmiEFkOCeMHNA;xAP1M zNpW6sjW6bFez=_9M7MSk-Me+z%g^lx9b{JP=bUB|9s{uooSEf^fwFJg4@l7s`HqhJEAWJ&=LNm^+Ol{2Ox8l z%*tm0(xc(BCjlz{I_r1gV&J}*$Q*A+*16hAd-}qSacl2vMh9-KROq`CqzVr6+C$xY zw+Z$xzEBS4u2`ccGyh!7^|!cqF_J2-j?K<-h{GBJ0|(R#gIU)q7&1pFe3p3$Uvu*n z1+3Y;pCVVkesL-f|4|bwpgP`__vWp}gDZK9)$kCLTzK-y%s}M!FsGQXRw6SoKpF!3y4H*eZ;mVwE0kGiu zL_ej+;~$`{Nz98u{q^n$zboVtdVD~B(fej1SI|(c8`cM^zd>2eBZn+}k+$rRCCbO60 z#pBekg!YcV%UkqSXg?E=YviW(%Pk89p}r)$FwMA+I-CZ_u$bMNqbh?WN}FIqTSE$DO;~b|0a-6p&2}ADWpaDFppIhL*Y(_mlYmpQ|>8Fm{(1awpEnNmsPX|Jj@%qH@V zM0S40>|-+TH3E0Av% zcvpgo7D4ZmO!6#bDNSmCX?DOMXgZetgO@>geq4@k2zgsM2#-29&8vO^kXC!=c5cGJ z97>gxY@@B%=O-eyvQF_-`%F}~+b^!^UVn;uy2f;7LvInxL9Wsz6Qa%qvw(!Rjcp%e1^vn^2B;#b!_Ji^Lu zyEQeBcz*_ZQ9sX4_vH27Wv!h(y#eoWZbnYKTC(!;b;~3#^192(TsOcL zSHFlo`QB&keK-I54%1@=w!-CP?F$iG1Pv6`yT+|ZGQO$BbRVoGl~#PBXNGKw_&^SCD4{n`LDN(lCItS08tXAa zVOSycLt-`hLx5>jbq5pXbdc(0a0Tnv`Sf88vC?P(YG8Alj?>YDIxtW79p%p;i}Mbc z_7-cIn245V8t%cKFQT379I(j@??STem@Zr2r|Zp4GO)L(F1w#F0T(a4n-tF6nuyH_ zU$i0;&sELL!@+TL#>-!-U2`vNXjzSiEqD&fL_bC*vN@Z+i9A{FNjML+J*e9zmxVY} z@}zND1Ia>6nhg3bz_v}q{rx%6@Llb(!I6C@K0cFrY{TvB9??})>)vD`JmxK02hVoO zx^hIYVI0usaQiI%+2>E_ee6&1#jLxY>LXNPYvr`B;Xu4n6BDXcg`PAj>dYzf2zx8b z-v2e?h#=&9dC55oAThuRDRFFmM%_3a6Dhz>iAuz5?Ew?!PfgJ?hiMb}S(F$fz4I~aa8xd+*1CwVkjaTE=-W@hbTdA+*h!;caZ{00FJ$|VK zz|%s`lal=*iBwWhEhxZZcPQH50moy)+&-@ok>Rtp<^aIE{rLsYE5FQUwH>g~|HfU< z&rgOr&uhAz69r(#v`Ne2zddZywsInu*k!{CAw|k}{4JAaaXBViv-E~?$}GFxlZAA9 zDj;izmW}1S_dEe-Ot;eER*dWpZjdMABYr5zt39w#o$17=BBk#}&L^DrJupZ?Iq8zv z&}|5B-Y9!0w4D}|HY&Gb-ZA*Snt_7H!@`bcmMy>%mh}Dfa1 zGwaB3HhQEHMEa8rJ~5**y=HO!MIM@vm3<}&L(6J|tSb+Xw0w6^DPOBdnt9;5{nMBe zPKL%mZdi5pfOlrpYdm>r z6;tN2Ug8f#XYOjlzXO78#x|KAzQbD4ww#(;fCgQSLSjnCGl&DkZi{+22i`{jN#$4e84AM$`PDs^5Fo@?J!>@NISK5DzCC&G+nBd)RF?bz z0Kg0=vBc`Sbk;2Z35Fiw^pGziIbo2NJPq?TEKG8^-{L}@{ZKRcOx}wv{mVsXo$B8o zNa`lL?g^9mjvCn(IvoH!M)#H=oVta6vQnPqgakXvXYr@|Z6R3=HsfJ-23UXwn|=JE zE1ZaJ@aiAH`I-&^fXc8--FLLVPv1q!O+6r_h=|I<80T$!6j7+P= zm>)dq1bE)6Fh}mEnKohRhcxen5e4N1z}8u`?ZpT=cRYjL)tUs*7w%W=0PZ!&9Oq7L z#p|uv#4CI6_Y}_pl?GY(v8zTw8TFdc40eVj<+u@E^etTnCDDpqu1grkF{cuZwDT ztscO(c!+_jddVLhu^z_4T)-o>;H@|AvHdf5w}pl?Ps+HnmDXn;o;TYh+HUY84O7cR za&>Bzht=};9+Mimtq&04-{RzrD_@WL+qCAczdfSAbLho_M@2-UL99S0&%6R_nnOZB z?_V~>v4DbG+RoK9U^lnEWQm}M_rpfenGO={WOXBdp~|QdVZDGOAh>^rq~`kJp;z7S zMlxLhZo%B`;kI?gJ4~tJBHI)HxEEBLntEBaKOHj#u9{DXyv_($>Mt{8#&D{T=iQ~H z(Uwke=RX@{!}CH4>-Z3(sy5fFch3xD4S{hxSh@Q=_b$QLPIIQYhPPChwg7DY(Lwd4 zh1=7fN()Md8vl{&i2DQ}*V$f#wc1MGC(;;{$ASTVpoHpZE8*kZa^Mo>K)yxa`UKSG zrAnR8kG9Kbkn{+ID$aOA^qvz+HoOkk+U;+$o@3;7MLj^qa1(ahNiV3^nUozwB?*As z#Xqc^kFx*)TVUKM$V1YuY#eeOwM3iZgbe+R9faj4oVY>ROb$qX@13PR zHTuoyWQ%(11|i0%f^A09RUjG$Fk!ro0~RFuXHQGPU|5V*3YVSFE}~7DGFURqpynGrm5)O*c_du zt=Zl+cj0S*?wE&}hJJcK-WVyeGm*i((QbA>?V~pz2D`^@2DSJc;kFp!Vc6~p_lHU5tsu6y`K4JZ#yb!Hh z9$wJgGHHT2+Gxdq~wBH2hN^XCVGffjODz%Tj{9R+V=7=y` zi8Ce2IoUE4#j|Bn-PfI*hko9PUG?MjZOzmN63Zfb^w?Q?Gpwrhp^I;4FBRQ+Bo@v3 zFx*IoiCOSwPaWN8J9Uj9LRYD9!5CZ`A7;PFy2rxSoXlBL`63$D&boU+=O1(Ey~~LuOyN(6>m+GAmR4TLDZ%Fp2woM5o<^Kg9yp zA(bR2PUl{=hSS_LpN59|W$0${C48Z@Rxq9)*P=rViISR-*bYq;e?>`ckWv&PnZx(? zi&q%!`~k<$Xh$VR32hH!{g=$jRr*TaY*wL5;qW<9tO_x7@mbm-c`bK~M-xE(B+gxt zPQJxv1ePu2?4r|d7dODM2<`D16`GsKdh|>tNTNqPp7jD!iw^!{-L-W94(ZB>Az!c( z8vKq_Oh^X9M>^iUBU3zVFaTEBQvaoc@^rpCe(F6pfbZG+w#kine)RR>1=L}o3xEd? z-p9#lIIin721j>%^On)pXEWi~`Y{>DF|2ksKI~!mwiEYtEj_?Ls@r;6$Qz zcFg7MhXeY{p{1c(f^%gOGoLO=;(DQ4(#pp)Q#EAUV4;7@Dk!lLf0cFZt)Htq zInJ`;8h%qJqRr?ZZ|7D%-mT7>mXY@8Ilda4?|%-{@L)XniBEQ34SiWxR4Z6+*`*_L zS8B7A6_EygAhfYY^hor zvaeZjl&o&&ubPM!*m78~bXjUws0A9pYM+zaR?nbTNsM4RBZ%148_Q{$YsHHK${bI+ z67rb9GmZHYr?zA)Y878rrF9m1qcu1jF%+A>Nr1@O3L4g#3PGIkw7=;I9q*6C%u{mxho7(5$TmAr?spXE+s81T9c z7M|`}o-|a`%0#lfpSlW$h;F61kNRiZc@saq1$>C%iR+7Cb7s>1XZ48 zIm9+{I=L}r!f6Aa$rBev7zQTWrmv2@VWH@n#uYetDOCM_sH(b(tD+RQpNc;6C!PS< zS^<2OFHnVZ%K$Ft4X8}g4m2nxbg~h6q-(chY3-g|f4f&`ATM)$gLOC|fJ$fhtcBO( zrMp%x=b_yy*taT$&F{G?uTQ&a$$@ci+XCFV=()j9E8ut|-Cswx1-uqpa4RXYEtw9^ z9P8J20b&yna!s~7!7%z27Jnj4xhmC=({>2HAxsb=c^c&}q1OcRC?g!Wh#WuHcPg+q`OVuEqT zNMhL;weeVj;>Ey1hhlU~(%FxqL~fLSds?iX#2FXklcV2H9o z4O!t6^P!^uMPZ?N$W+A?XgR_k5mSK#efg7REiyhy7Xh8?5LjSl@{*WDX(#EcuUQEd zwCk;Is7<8i@0DKR3{q_z&Y^u)P1(j!%SUv}UIuw4)9tOEyH$QmR_kT@vH@n>yWd#a zy8D1UYhazutL<-_>1>{Ejb2n+J)&PaKAsuPY1JNgfmF3iwM<(|$;;Kp@p^2@v7oia zNm#vCP?SWg1WKOmC~UcuABvEx2;;KLuIMrV88HM=)zgRK+qGT@-Z<7WB~EBZHEc8n z!*szOrBPc><-(QjKJk0%!4X|Y)`Qq~Qgk}RBu5uU5{5D@#Q8PVh;Mi|N)A|nWT-DD zVM8}-oZmkDdiY5YxBU>u^eb;_xHS$jb$&fcYRch<7U21lLtM)i0iwr2-2Ql@&8&|z z22p{V6|8N0A49P9GZ7vFkYPiH?JV`-S;n-++4D%&BXZwTs#XnT$c0=rt3EzU06qX# z$00wLu5)eiex~c2-7*@wpoZ$j&*`?R@7CJL43~zJyu*0ymD~qBK6melFs9)TUmgk| zL7beO3!iHctJ+AlTa?_Z+Ay|@j)ePJQG&cm$Kp$JiSK_KKxE^d``qBNpL z-Hs<3iW|uxfh&TRYw@8AKl_`hnr1@_P)8(>cHXihl5~=L3b$=$GSm4w`F7amj1ZJO znJ`Fvxs3u>rIt(2uiX*E-xDA#pmNS#lPuL_FZGeZpQ_}ghq6aGX<@P+7{wr>ywzp3 zPXN@|j>~3qeV{9|IO00ydTsmcR7d~qRLM}rrAkyIThdtDFviWU`F4OpK=KW_uTPYe zKH~=)GJFo*uDYXdMf28Sde606NorQZ{_F|*h+>n+yqjUEHv`-_y&r|W45S~89AsYa zQ!Lhp=f4mV#rhz)UnFRLp-DK8u@+qt<92@aSTM;jfph=xIHq51a`OJ-9#y_jNbAD= z;)=w2jB_YBUzTgRN+sDV?wg{zg||a?SyjtnJxBeH%@nqb?nHiXx;I>M)u@AXrvx(; z5o1lGsj6{YRXtf>^efB$w-O3QFMWgHa^x2KW|+-E<)LUmeK&0O7hnDkkZlwWy`i1| zDc7GyZ*{sMWi}#k)%!VHelu`q!>S%h56+L4lrz{LU3a?8$c2VvxGd}dbrH*h+;sz+ zCR^VyE@ZvzC~z{YOd?X^YOz_k)2|HWr12YuzAp&I*HaNGF+tNVR80*%16RnaydFpB zhlXugGE-wVIcTtzTi1|NIZwGy1kGm_Z6-R%Co~tEb=W4vKOI?NK_~GUykaldi&##& zMpCQ4$)k+jVZ}#>GR(WrcmX8F&E3rWGC5xzvBJg9E752)k z5zpabx~(I?%VtsxW3Hn>b1OLNs|1BEl*xl1HUIkN}y3WNkf)|-C z){SDST?0BU>PYk9i3Ytczoc2fUvlCwNvenl2fqqN z#+vDowa3EHj1m4}eZKR14~Rp8+xxfK5f$26@XIroz2CenZtOI;5R}2bVVHWU6{-3V z8Fva8z$lP8Nplsv7E;DGfc-ozf+auaGCl+=Lcy@2)r}Q>KT@fYllt^rM0@{p?bra( zwxk0g;}I9r%8l;<>*;<3@1Vh;tBHY>z8d-M)pLl=h#X@FAl1C}ZIK*qQv+LiDA#Tn zDjy=(q!4H3oXI*bY1zcBQkCz^iVt~<-d_%#)$XqKd__3aD1m*VRhi!Va*N@T`Gcnh#)OmX+Z&}3y&srigbPwm6YEK% zA~)3gCDTGV&&{+x^15T$p>Fa;miH#O_`f|iKYrT^^+6E+%9do%^bRs5{yYBBnt+#Y zJ$ec8D;UGA6e3^x`~G`E`x@_1ZvzG?yR!QLqC#Bt&;< zZcKn@-6&F>fy?7UqcjNX+r|g-DBZpEz65rfRgC&;LmyU`h{sXOocUKYmK++Xo093| z&@-HsmqUA-X5MqxC-bixcTTZ=SR>KrIuD=+p=lHEuU9d>2{oWoCs4$&ryu!@6!h%* z3jKTS#-!-;pDPo3*w^EnwCHS(z5cSeAS|;kk+PnT2DYpj-<8gTh%k{1OJ)g+9=m8G8R{h$FCynHDkD}` zlaIf@1I7B9hEbfh%MPUZ7TPa@3a8rO$ETH1&ADZiu;R z333YR!E#4u?0u?>%YB=t$xi-vn$ZaVqUR!6e~gTGym%CZ1t*sa8i5t&KX?NZ_?z~G4L-Q>FoA{ zgwV{Jl<)1%&m{IrE_dq(msRbma>U!Mc1*O=`ivq)gVB{oMsz0F>?U>yF5X9*WHpwF zm8YJnfJ!mtkSG`IQDJ>um{&urC{rY0*YJaN5G2=_OmbnQdTlx>lY9riC#V6f90<(N zNaiksOHDf0m*wTJLDm&ip)+_Yn%H5F0FTHh^+kv{CsON+zX;Fp@cH?K^Nuab%u9q?0x7R$h(&^_&O~qe_5rbaBgU!X%7bS5+FPB0{*G7wR zI~neOLl813bvP!?!r=st8T%G;Mz5$lhe)S-$~^-W6#ClrCGx<}W@CdYpIV_h9r?{B=vjgWN8^EaCkSU{Ly`rK%N>O+kx+~C^W#;mdWd6SLWeNKKVmLgDEE2Os5yh`x#zp*PCF}FPN{PWyi15} z{muBjsvaM3>81MaU`=^Q9| zjAU8o4pO6|s3@BCL0fNIu zbRx`c92O#&9b~r_=*1l8lp5xE_z}A_%)KCO1>9w-#?>yJ)lWBWaddA9STw9}+84A7 zpx%*f*f!@=dsmyL)_)t^Fa6-m6%+3?Kx}MUT1r;Q+DW_GbhSjnS4|ViO^rW{BTS?N zl4Y0<<$P$~cz9&G)86++)Yl2L9}$=qipXgfpIZD9tonyeJUcY#Li&!p&-`7quvGdY zoaWHvv{c6a$NHV}F~RokP?Qo_<=JZ9k_irnUR{ofolee{7pR@O4*^{(4GATxp4H6S z^}-s>x>nJ8R&A~C+BsVTzZ?|#`-FqwD)D*Kq^0`%6wIvb?_-s4QusdHZI*)uQ!nM358@PMW zxNO2FS*hR3I}Rny`GTG?gGVwFIVE*XM50Gw@T7BHaLfL**R=XB}LMVg+7 z+%!01>{t6W;HvdU*}z&>G+Sl9JB+AFQ(4tJ_o`P59AA%7wfqs&e${Z_g+$&WMc!Vl z0_?IKOVOYr_uQqJY?>%_hryt_#L%)LnAL^2Hh&^SQ9PKs>^^K{TLO%rNKSu)A01So z#!m08c&4Km%SD{}zg~dV1r7e`Aj4{odnqtx&K_Mil%os%kQWYUPmZfNm#jp_C zc63>5bsX~0rX)8I@@Kya)46Xi^0KbtF;u_dKk<5ciySje=N^A~@6_LHtf%<4^bG#8 z;z)Ev@UF;hCnNZU*yov-J=tWPLNlw*Q7p~Z`W>IoZU{Jr(fJhZf}GxU=H!=jY&fO5 zmucY3v*@y|e<%T7nPH+acbfiU)#}uy_0sRQaa0a%yADS;1PJ_**I_a2 zR5>dPW}3S?=WC>LNjh$7O{*P~Ry(Ebhn{Kq_#*W+77F@2b1+V-YR4P)`M_fc1 zVZlgxKV0EauNVj`^R*CE??b>v1n;z|Xw7MnqMvnR&w|mMCPR@V>ANgs&Hq?-T5UPDVsB&N$72t;#f*%RWE z$2Fl$zeK95xg=ek#O}*>X#lEH3@!{d7}@}tKQ7$x5j{V(LOaAqTo?-eca2f?ZcVTj z-e2i{ z@AUU?L8YYpQ6+D+4_gYno;(GJk@BR~AiXaoGW0E?J-=EWGwg2`)KMjS2oj(txPPx$ zn~Hr>)?8|tIH}e`KXC8AR|uE%oV^&K&SJ$kYPHZncOPdON_m%J0e}*mdQC$4yPi+& z76AMt&S1mp?s2vGVylP@QV~772@%X#0*Oro8jA0|90@+p& zCl~+SR>1LV^MC#0d#i6JuDM(NxwsFvXur9~ojf$pBY-WKa;Mn8d5q*WUAH1l*?q)T zu;|voQ)}#_Ma*1u=!z-WO!*0n#_G&9$VD`b_r{mr0II3%ZvQY=vbq6;-2zbfMyD_S zIoH-kUF@(s_%MT%ASeRS>79Dbw_F{Mgll>1Qm>KU@?33bodM4WvTPMU48ry(vp{XN z8)^ix@=1Q7(Pi{;dG@fGCoC>Eb)1m=-4l>37W9_pD>*?wX$%)6{HpM!gyKXt)Th;; zQEhvrEUn@|F)0#qaNhX+R=|&m3bDoxX|2_Zd-I|M40)@UhpI{EzOeSRD|yK>WP+)& zwAs$&irF$IZ#MY=M^{-bcPi{G-({=_iV`f5TlNFcWI=!94vhI0{w;WU2U5>;x*dxq z;^S7h(9rwcy5R!S#Sf3HLC``n*uQ(+xaDuKR5j5?Wsf!Nl~@CM@@zq2i9^IOXXilD zJ+$!J>_%`N0Zryj4RC6+gE?%!lPwigp2h27~|tJ3T)DtYwiT^Vcb_x*Wzd=C|6;r{SJW5GBX4+ z2{`~kBR=?ZX<21BA9(OJ0AMUKgX}=0ZwBjW1)_x$_HohI4vYj?;lodr&nqxlZ^vNQ zD3zRgE0=Tnf0;mYq7e`8dpCvYCXpn!4FhdmLRdlk!l58-9;;l%X1B!J; zUCuoO=|>ubtE6wk_j^i?DDv?_SQBh5f@S;mNcf9I`o-$~gk;)qG2PLjby;97h6GyO zzf*&w*$==hb-rxDD%e`&v6rVkEl<&_8R>zs&m5J?fI)9TDi zyclyK+ZLAy4btdgi(Lcw3xpWsbqEyCQRtPJvRHTcZf0Cl0C1KzX+ob8U&b2krx*dq zL_Q$bMvDKu{c1kr^9F9&$!o1b^F3`GniSy0cTy~{;P{B2wqO?kd@{}QipRCU`Q5i2 zEt}g?o34!7b4{C|#e>mT50ZvitxWd<^8|p*F!;wH=({LR?EW^g%~Q4s(Z^l|9ehKya{WHWiWe&wNH5S>{bl=B5m`wu1QhAbF#UY+8J{21LON$E9F zP+jkOxnW>49sT+4wVYKkb-%BR0s6tCvke7 zZ@BG7y&3oH$DnXJ7$5oCg<)fMtdSv`(GFTXQcDTkG;qt^1q$lgKX|}5?}4VjL`oH| zLYxc79B~o=g8QBhrRQ;E8oux4^!)?qgmr!9y!d;Jx94hKtPREom(>sfER@p_jWY5^ zPUp2@Zeig>!mtmzjHpv@>WIL#h#7bKngux}xblKvSo2su6-pTv=^caQ-yZ zdpMDP_^AXx_<8z6Tgk_wQUE%sHIc?P)?WQEg>7~9i}(?#*>*Nx%GO@+K7G0QP0U1F znI*|Fk$pF~oKs)^lwk-V77Y+$|K`V{Cj(PLF4S0)k}Pi=k{)D*oulPBt^Y`iOZEWZ ze2g7V;kt=$AK@5E96a#xG$I?$= z1-TLQJJ1}2uiz_G9atpW#)}=v2}B{jbqL);yEHP<*-Sz_){VhX%F?`DU!@BNTh| zAOq&oqU<+)DoF%Xi=$!&IyzceRQPK{2!KE&{q$xCWnGitM@5BRM=R9{Fr(aZ2PaN` zt9dv2u~SlR*BHKPZ=(1v{UJuPiBeNZY{qxIEjpdN!rKg+xkbT zq3s2pUiVv!<4mi3>UH~jq$Tlw#+&O@tUtq#ZhF5w4Ml1EbB zx&t8nw4-u^!$+WiZb{2P75bAxmIfsY7w%#QC@P%e^31ra{Wiv!1bmcWDI-4H1DRGz zG2bg6VBz_8lURJ)?w^U2Q8-Lwkym_1X{JEhk9giNmdwE6&q~!YzDehugVj3(%>R1; z@G-cHzKO|<4%-Thfw;V=qSFfAmkBkhb=6&`ZOrGI-m9j8>k|eudTMn7FD@A_B9iss z+IvfTj1e?e-_|=>$NDSCBf4mVQ*?UwVma4P8B-eojI*@L`Yr$#HO7BjoFJe892Mw8d7rL{7J8$%mF4bxe-mYU z@H7J58r|V3^kw3R(vmU@GI(1*-K|p2yYPXlahN|Csqw8YRw&Zfw)C4GX&zpFrBA5> zg}0an6Dk=l=>}lNt=hx&I}t{C`mU{7T`dch5W@9PLt4X#gDj3w-kj)iD$5^V#N3QN zxZ_g!bxInn+6=vKPjk_>;}iE;j(9$Rw(#$ulM{<`P@L%Vk4oniKGk61NOVzEt zmr&bzyQOvh?LdsLZSikt&Z-|=NqDUF!lVMk8^s4BR5`M@1?0%ffW z13j2p{EBGsS^yt1P@ak9#OL+_Hmij-1Re+Opb{|DIZL&Y)}x&e3#_H3*Fne>((9gA z69{Ai?Ecn8JlE4A3(-7h#p&YjRRl3i0piMxz+h?5A9Nm^8mbGlgA9-+T+pZm4wwV)XDwkd3uDyG94L7<2 zy0E%lXHHp&S>y5Nx+!MMMiF$!u9ym`TiyZB;+M_x^{GWZQto4|AbRb_@ipoK)VnBv zyTW?K5z${jc)@26fbII@*QvjRm-8&Xg9OSp6xr|wzpDYPpj6(c1qeS4{1KERX!6wcjBNASr z#Ln9kyOMl^*p_?8-VNieGC6VnSnv(0JWEfldG0jXCDXdd9Rf(Av{%jBYYqS=5zB;n zTFxGT7QBgxU2!FL&tEa)V*aL8P%9YNvE`3w}3fStXi1 zKr%zUnBpGih!*+0P?)Uin)-0|xeI^eBV`o<=?5cIcb-7`lDgsD*QuJ&tNWEaK(G)s ze6s5P`KsC5dM9*(-^+=%_)A$uqhlQkr@(mF>x65z1Xg-DxVMb@{+LUZeU<|OsQgP& zmZPX`mjL!B#@Mtb{-GNZRpn+J$q;h~uv!dbb*+9!6-R&_Mze$??}#>JPjBm;vJXr( z?@aj0$3#EEI)>f(Gbp7xLxb@Xh1#Kii&c`$;F#BOZxvR8@b4>u-ye;$2yc@q)DQS* zN}h9f4ceAnG}SR472fgQ%zrc^8wA)`i_ZYYtqd5@Dq*_L`^FR~5jJH_Fgu(K?=VD= zvb$YV_f)^Zy^Zw%nlo6@oI9cRK47;)dG-<<+cu$_bx9&R1AR&L=nuW9FJ|X@@`) zs=i5s;p}0-!0l}0y{X-N&pc*x1}nX$O7Z=)lDUekmluVtk|HyU?X$RkjMKA8Gk(e% z`vA#uB(I-6Ay$rt2^~jsyZ$C}RN{MNU@Vwu`#MetsQh=?=okNh@Boxp8PIB|<0gQ> znI$YcubIPTi@a26}Yj^I?{cw+7`PB8K3dXF$63L5Q2R zN%+O3z+5w`f%TkD>TP*F|69zMDeghdL4;G!BN=^VkiJjH%I8h5i8*8(0p1A+bd@#y zjh`?skiYJW zrkRrM*Ab1Ggr2i_(yx2b6M1|mz3M=$<6yK2ejJG<6&ZKyp=a;RGbM(M#6EL6yLHXG z0^*OThxvCMkzeRCk$M}><;RT$;$I8n2X^FZG#7jU@K$SO_6mc&{;!k^woNuh$OGF& zU6b{wC8i80Ndp7)6uvL=caNidPTz%q*g+Wxo?Y{fsQqyKXj`-Q;*87v{ULL^R?Ns8 zbRPVjIpN1<;CeziO1yR}oO^VRHvPf*<=(v+gU&T9gJ0o=Kqn2I{}rk+g4)F(dYdL~ zjcm)mXA@%8IB7Q4kT+Ex0Fao@(OwtB5(*EFc8A2w*tmu^Jz_hAum6WQP4+8It274S z^qCb@)CZ7{vu`CH7_9z-P4J%GwzJUufPp%Lu=9=zPiKaJu>G`gb(}`wQxBGrCKd*l z`|+ujdrdrQ8Kc}f#B88}Rve%M=%_~StS z8z)nZX6SNf%1eoY*QjhO1y(Ues!7^DQ2AprQ|!DOU|4*gDgf^lyc982c4APs+_3ti zu*F-J)nLGzoV2U_OVVl+^Ap;EfyeOS;U>>3{k0#;=^8m*!pI1sw~RA%H>FtC>Fx_w zE)5lL9l}9zS-FF_b_Ju*J#QrtZ@lgKOhpA+lT!9P?=9rgvp}JlnF7$~t%cv>M}dGN z-+M2L=0Pvw#URhn+Vh0p0k)1q;9(|a9MPT8eVhL@fD_a+jxw%z59GtojW~ez3oyON zM0kPH_xuhxXim=Wlx$g_DW;3ywr-P{8g}SBuqx(2W)6;vwV}FTbfiwR;!O=)Uq7th zg&hg!HO+nrBG%o4;95=55$g7{5lk0t^W5AL!AZH9VRX497lVQI-j!|5XzKP2a{@A| zfA9?|QDAC9hI1TC=)XAlt>!@h>0VOEGhkwh7RCRtq$o3uvTP*4!{@ebldXO`y`}dJ zvk^2E)D-SYy<8p2Ch?_6dis?{c=bTkLak<%($eJMsBB7xdDAN3R3}`*RJWq~b-QfJ zg(bQgH@&|Ij074g;2ED+G6#FvZ$^q3ZGRo6n4@J3+6i}gFXki`0BxP$XN{~zf9Rm0 zib$_%otf-DHR^fJA3# znz2)iMk?-kmz;I(3&YgOFVfiKr|-3yR>zsP=)mmeuDi|cJNUT4!L~U_;?U()oZd#j zC_OpWyYHSK1z=Ad1|KhgWcizro?jyl#h7kjYXn zasne$LfQFgO$YwBr0S@m*w^s%}K_3`YE8~|r&k=~?q~Nyv zetDdL$?4QJYg*I%lu__X$Y4Y`j8g4xW>0f5N`o$w9q94q97i^A8g}I=uMcb?{UKDn z?z&o_7V`+sei>LoRZcrameiJ*=rHBDO4Em$fWlbePmO61JkU@@d)l`9&VjS{2~CaO zn2b7#r;^&5BytK?t*&Wj6z@#9>d?Rfm3hJVk*c-QNxC8(LKBjbMpXzG663I+gl z>);&v$;-D38_1<$D{0Sf^Yl6}WFK|23W}swER1wW0*V?;M{(A%{F~8zqVgm7Lwl_r zA#FI?Qf0IY?`|a>=sW4^r`e49Va`3)t@J+w@Olrwv}r!2b)R!xakL>+n{s6yK*`}I z=3HVy#aO{Lm-{+aWPWCduu5IrCV#QBRgKU2cgL}S0xwbM_vyI`Cif$L7|R=)s$%`8 zOOWLio%k{SJj|fA@9%e=dsQnJEr(bs#2Tt6>gdnQ&T|I?!DJ6}N8D3fhrfe#0ocuH z1DJ~_4%U?ZZx4}NuZdv+di4)N^(-8mdFK1E; zBMecvUl#g-74@K!+6Zh06q{NGoo2=DAmk4<+{k4wKmEC|m$>c3a8Bp%8|rkILR{3B zdS!=nYy>txqj{1(2nh~-riVglw~s1>5BDZKMKXpa+bM-pyLf_da1^kWw~Z(v&_vO41*gHi;~@3X1UDzhgMS}ukz7gRMu%`0;$+#20DO5NwP1|}TOn)4~SuhcnEfLv~0 zQMbBcGbGa!eLS!_@}XpM_me_xM*-0Iz4F!NU(^h#6^o2`b9}@4c~N{s&T58;4ibxW z>fC_{`$(TfHs5;Dco~Uwa7Q%|x1`?bOf=oXYXnb!u>)rO{PD3}VGUXc4yd!g8RdLjsev ze<%)HP_IQ8F#y&KX{=Tw7 zzhS2ahWrLd+a)9aNlTal18rgsL_4>WU^Rk8<09O`c8V*smUPpgnlF>}BiDkqXFiSI zMAy7wIqio=7lsbpDsifZ7ply!k+O7jxSqO z`-!|~==b^2`OJ~zex(?0zm^ZLjVt4#^o|(RNX*c`>jPvEj~_Kd1sZOFxG{vEXg!%B^Vr8xhVU?_rK?;W+91vDRZZN~~FXbZ+OwCDamTU?<*f7dMc!hNXiaHBk zw|t6o`}iKF3vyX5l(S|b|`mh zG^(hlkp}#0Q0>08uK6u>B zdKuEZKNFNBi0;`FkBU7zct4kQxI6^u%ZZqbsg(w+h29{< zzTCbLRMh9|KPe@}`pRVIcw4Bme!;8M4(Qq;NUQV`!>iQAu}R$gMT--4n|-5rM5Vb{ zrEe!{N8_hG3Zq>dxXPLOsk+GA3DLmkC)iqyh)V#BODgy^6XpKx)my?!Vd8LpiN&m# z?%_CcOzW^dplzq~{NocgjddgLx?(LTByk@RyR%rWQU@0ckIS)aS^@PBns|Z@a4NbC zNY|HwA&H9pir1Q7oNshe%;0-#M>VnQtfn{?J0-iqEl<9^njNmyJ{SacsLsl8X@VsC}^JE;adEB_BCT!FE=y!ZIvR|z|qv)r_kMvyFdT70h-}i@l%NZQc zX9)KYP`4TlAY563r1HGEUw3d=#^CAh;uy#sQ6pYUf7f&WR}FTWA3}8oF~M2kTJGUf z|G8j-+$bWM#;`T!mNO)^nrKy$Ms(+AK}Lx>*NSE?D!2_8{(gs4cg9PPik~(g5H_BA z;13z!9Dg1wxZj8PIaiQT?<^qDM-iLY4QOWM0#VC*poTK18DTQX;eJWcFhj4%Z4GMKok{I@qpem*63K z`D+dh;7BFIw|hc5`OSxgLYhG60v4Q1W@G!ary+K%eAeHFCt%) zIY;Ju@{ZEyI*DE5zf=C-ef{rWaD1V{XIT=i=gZwh2wA4P(UTsY^5jG&a=Ob3t^}K+ z2Fz?a);tSV8Jmhi7WVD2@Ujs!(rq`{KG&7%P+P&;5NE=%8tEQ_wo$&NSAF#E^6_r| z_-xenm*rg`QN)~>2C1P3*NtT*Xy%+N{f49inYWMJm*K3|U><-`vmmNrH+74lH9;Ee zj&I@QvA;8HqlJVjuG|zH+>&xp&7b7|t8OqP1Y7H;fTvSgtZi*kd-zAMc(?i})<Wk)JeIt;?>yK=5Lvk6uW!h-1bjd1 z6#KUT2G>s|%x`)c~s9f>Om$8(m;5su+N%P z$~!BTom0}rLcfOYG}5t06S$h6$%;N3BiEepdbK@77p4qyFhzJ2SNyZFV0h0BGZ`0 z^s2tE{V&T(F-;4uFEOE|z;mr&f$tKwX`{chhf`>ggUwxi0)u-+QXn&Di>>^;e{0#1 zG#(HZN_2Yew^Y%F?Kj`u)@n)ept#N=yjJPto}uqbEQQR;f_(zaE= z-d2mxN6YJGlridWn-@+pT|T*Nsh_{D$gq&cvA53ngVnd)O{|p@>Qa(|!JY*?jCB`l-m=ly#+<4ArB-wGRKHSBp>G-t{KPLrKB7}M!BTFZKIKiqRP!61xr>$Ktbgwkx8+mA>C zTQ)o7p~-h7ILJnCKh+*(WA+5_3gKc?&0d5b3*x445>qtk)V0lc z48iz~WyvyGnz`a{*P|05Vx!Yz4ub9T$X$CZLxOLyqd-a3Y-!y2mk2~5U)PHCI*zDm z7tJ{&Bli5vJ8d(Lapwz+%eRoHxW z8KxU+Ia(l277%QQ7ypvR};vlZJ2wGT&*NXir79@SwXjfmI; z*4bL;`-letkYH)ED-9LcdaoS~j9V@I6+d&-d<6i^N>S>1JVC7-@puTbR zqYCTx$iy?gvoNJlXp1gHbp)?3Q(pYPk?+5(2`+rS4C?7|?V>=W3VywA)fv4y_JViC zsFtqUhgi%7+^M{AYv48}mv74Ti}W-*w2+CI`^|Sxf>BgY42ET>^GPYjfI%4QYOiyJBqGLRqCEnE=&NetN)H5{_mHc@S(1FN2T7;Zq0FnVQb3R zCYt0vUl0>_**+}H;nTXY!)sNzTj02|#Bht0QJx*pIYlAh7rtZL2yAoBWmPeyx=vTHybD_hzZCE`^ZyVk$DFp*z0FTEfBK@f#9Xyw7_dTwrh}hhI z8nB60vFbOuR-kt@>{+X&rMIILZ5X)rh9Pt?o14W#dJ(#C?0PEkoCGCM>L>9|T&J4; z%~Z29pt4PO5)12EwWwKD-E&X}!{M-SL?}^39m!W=BBEE9r(72nzed0>vq{I4GF{^*u zlJB3OM8{|^>m!NE*gg^ri182Cv#XRXkm*^qPHQIAFCHv%$zUV!xC$~hQK>VQRm9LU4&H5s-o?N8nOX4Nu|8{5?~Rq&hd+lbVDN8cy7l+8 zMXhdDB&AP|5lrmqS|9MN5B`)BoqHed$-S_Q-15hQ`O!a5NRhf4KWkYCtGWYNP<6{{p@vRSnz0|UlkZ#Y z{gXw_1UfKO7aao;eqvU*DlO-DiD*}Y>QVt)?}EmB%<}>rW$xX-y>h-#wBS~(JVN;k zBh#7yq@d6$N^L8w*(%VfUrmNqf`f-7MdW_**Vv7;V~Vk6r)LoQm>6S-6jsWonk|v| z!HY^4TbeOD#s*S0n^U$*n@SNxlNqIJym0 z?+1G)F4=UC(F5bTZvZ2fEaD!SFi(A?851?iCOx%+a`o2~1RY8T`Qwx0_#Dyb7A4I< zlmObuz&TM~Yegl^ey>}(^We&NgtJG@2ZXDcye4KAWX87Ixq6FZzH|4OjXS53SBje zJBlvGo&+z$1dw{!%k#As$zeyjn*tED?G-a2-? z?o#O?pp_?L67FF1@jKSs%wu0OFm9M&UoBP26SrF3=d)az-nQJ8V^`wybH{>2=|EbH z#IGW?^g3%3cvpKz0I6m-y~ge7vy0fz5J&LqG3i@G&3|y%|Km9oY5mOD6wJR(TNLm{ z??}Yhc$^+P=f$o9m-HcN#{QBt0kZwPV%_Q48IPB%C$_=zWpZs})wV^7PWK^RtLQWt z@;bIhM71hY6-=Y$oldfgk1}sO<|IR`3nu}=X%t&k?Xrnhn{Z-@5z1bV zW|Wou<^37eh7sjbgG#BJNw4je<-QPPFSCt0h9BknY~vg_{Gw4re9DLQb!an!Ffvar z&X-StA2FHTQcb$Q>BzZXEn?gVw6usHjTGRlV#BYwAFg}Vto~s6&o}i|+~DC_SH`NW zi%q3|()^f1tN91B1-zCYuRKe5-6eOu7Wpzv8iV7i7W-mn;zchZN+MlK*?rUI)pbhA zs(zdEa(KxKL4@%e82@HcQ?+PguwJ3yP2X7-Y`>{}DoC^gfd1FT4|CIO;k{hX%8_+< zwKqXiDTB}ZO9A5X$5rc#yAdzVe|CE27=A!8ys4z{MM*C4dO$JYv<9IfP zQUqBo9e-U%8+s-c5}t3`y^JzxSBIcyg|A`fN#T8iS)QQ?!?Jaq+&Lk?XUg$$ZD^!M zWx8$?{R$DLFLe93_OOzNIZO{Gh*F5SK)$3;3ZY|WD@ch+p`*4Y65dbTh5D`G+yA^| z|8+v(K&Z}Q7cd~-D5vN$^_FXXtek=juY9JC_*TZ-UVwPuTXUR~s!-|UGJ(2Sq5WGY zCNs_#@;i=9pK1|wDF^$rD7W5@A`GM|izF4fBW1qScBDiL8nc;QN=0E6vJ4~SpRXq^H$cz5O;u2T55$nqB-ar*N#GEyToe zk~@AWNWP;!&6Q-%^61m4Gpm8BkdMw;1pL{})gn8J#A^hu?93@NFgW0Wlamk|bX- z2PrCkDLv_rm2L3&o}vuyM(1<#0!^~N>W!HJh5rd$uRECbnrkg?ZYwG{#r|Kdfjr64M)mPg_HPr~Y##kq^h8BnoBnr6e| zN5!R`q>$OIslF@ORW(e^uFmBR*gVH$9%$H{mD5Xj;o#u1-Z7!sHXEixoBU+J63`J$ zDF4e@>kB0eaXL%!rCuDLn|w-(g=E@5EChM3L1G8SqJX{e8<(j=op$5g%Q5@`$`|x5 zHlt@$-3B+vGFm3rh5%w@Em~uE@D`Fw)&<#A-WwK%KU)A8e7&fkOl1SNwMt$VOv@Ur z-5}Ygc8~b4>&2d!o(mY`Y?$N`us=*-dMojpEXe40Lz9sL`%mCLM&Ls2#wdok=N3!f z?Gb#K!FBcjvzyJ{eDjk{S>Q(&=d2tNx>W#DXCdwZ;%pqDOb%ryDlH?&SmX26x!aQY zK9OgQ)9$3uoxdBWAh`PyI|pQ{qxj9Na=1x2$|L$7eJMa8Fr@nPs|NnY9`5d;fY`Ox z?3Xm5V?A`v!OFk{yAJt-S!Gvwts#i$~#{yx+d z5%d7TUc(OIID24i%dh6@xu?Q*7PpDnNynaMFY`vi_sIo(BV&JRZu}FsHAB@}CQcm1 zUSN(EL!RE-thilELus|~6x4@lw6V52&ejWQ_UKu(?G{=s31QZ@prpb*!+@{jV*5q@ zs3cy_u$H7BZ#ku6+tFv7IFR}<^q2hH&rhy z-2N}(eytJI^+oCL9^1~)y8BR0O(MRb0|z)@&3c626Mdt?JI1n8jI&c@bZCvkz%BIY zL-PwM6_oM*mWjY%`FUvNW+ka=kH<4O5rsBpCU}o1#Yc5l*5#eq$FZmt;V*nP9#Kop zD^pO<``9W$%6Sb(A{UtH;5!8K=5b{~^$RynHZ3F_<^$x1iwdHv>zsu~jm*E8!1)`= zxi@u|8K;Y!yEj5rQC(}!bGL$hg!$PZSKJP4ci~hov>h}p%@!d%CE&)gaU9j&vD?-Ala(&R-5fK|ru4R<&(BjlpWu=mxiA(XuMHOS+XHY8mY# zm$R;w$AGF^l>Nr;pvkG!#l2;+3HGL09~SM0aVGOD)d@gAwNZzW#G8_tGn4}|FXET^ z_t2p-OpxWh+KQRxpI=U3mMH@Grwlji8pn4@kA#?B>>4i7CdS_e-p#R&?Ch3e1P|K; z+0tkjQ$av@kc9G3GU3#K#|a6VO|g-C4;}wHF2iy*$34DcE8Io6qaw=D3>)OM}HM9hO>@|)dIXZY1izrqO2X|&M> zmRg35Sdh5tS%)Ld_(Z%{p0h+bt+_Ls&AQuH&}~kqI6Z%Clp}V6UJ(CVC$sXzzt|WC zl#Wn4<}2mKqLPh8iyF%>9h(*4G`Ce`sIr2@a{f%{sx;*2#a-$56FVS;2sW_Me zGdGMUYXUk{0Hhh~4wD6GS7*Ja*kB%Ng-*TLuPB$lgizNu5K*kt9xM3^^j1@w3#=+}_yiPcY~%ggzuzkjU*%L3e3( zuARzN2BN!uht_XpAdc5+_!dPFf~m_|I7v9~Sl3|IK8ZIz`7fnO;2;(h{gfpIa)`MeWa5iTBU8om5GGnWl&H4r--D>A=wJW?ay!!N^8Kr+%_o&ni*(WA9Xz%u19< z*?T$lI(6_f#j{3}2FVQ!gneDz2HoVIo*MP!9;D#@Ao7%*k$47~%&*7BGZxscZO1We zgMTPE{8yS$FG-^Fj>v76Relu-O4r?@YH>>LMUa<+W{SqLzg^A67F}m-0Mht8xXs(C zI;|8vIATfzs@tU4mr?4G|U z+^1VK2MSbGANbgTdC+a1bRfZa+AYcnUS(FTlk4iv?buR7cmq1MllU??__qsWCkg7H zfwXA$)X3*n1}3L@*Rd@_NBbKz11*h9n|j+l{995?;J1vDRS{dT-opsOK)p;0*`tcy zn)~+7&+=AEp56AzC7@6aNa4l&?Jb{%hfrG$RHlb$SuibN$o1NJ=UrBw7wNT4I2nGB zdsA5-hv`w3X+YG?#wk80%E8lK}KCa&v=m3#Ho!0p>5tw5XJ!2fLO`( zUjq7A!FwoSo42_(*=t5j*oUu_pa2&x?h-c^7iZVfQGobAizq_bkF ziLfQ7xEVwp^FusJ-bY-rj*vYa6Pbb`Id*fjjRBGC(0Jv%qWUR6118)mJAqtJ&s)1uRwsweJ-a?)uD4AYxCZAj5%^T)hap+$ z+7+eh7jr5yyz)DwpfFJTvtQ||xm1`Vdg44m`d{kz#hl>kht}%q&)n$?($9__@O4LI zJCw*aVO@H^Z88N}tMX4V3QCvhLf0Sk8QwZR*Y30s!hxS5PQ5_;dU_MRPd`jC&Ayr+ z`fCGRk=>8tS><6`5>m|LE9_45*ZPPgO1cW1en8?&kr}Jwu5DM zv3T6h#VY`bU>*=MF0pAMENTS=9C`v^zue>e=>#?B@Dp#%%Q_3uK2=D@vW*%)e!oGM zCi4uZ&83DVDpJs3)S~p|Ld{T}t4Bv;q}H2U62F%(c2usGXNgQ>H&iG9KR;&J7|L~A zN79t4VTt2Np{Anxm!Gu3%M(x%DW%)BD~5(1n7#!-BD>_*#uo)P z>!s@GUtv-I*)akom1)V(*K^PIJWGOR4YN%Z6AQX?s{y|BRue8<3ifsqT`QUrH&9MX zCNuI>c%pZW4r)*##23tPoUH)??~$H2QJ{F8DW-?uxA3}lqub_zDGh{rx2l{4>Ux@a zk1jq@1WfrxmpAknGA|rgbI4-Ly1!_WztaU(h>SlFQuQFQTMFY?_XJKE@pC732&_6Mby}_DEHL0fXQ5sr_$SF?JI2Rw=d(5N#1|1{`&Zzq<|Mjd6bTLraP@#6Frvy?Gpy`%Yuyev-DO(!;vtU?k*x zu~MEi=GYAC(uYm&$5TLKg{TUJ-z|8Rt-<4l#)Iu?)7Sy&zr6VW{aYX73%7pbJYLWG z{^XX5ZeuE>x~wDj+gGu4rKN+~Uz0W6njrz02ZfV@yb7gVa}J3>4S*MZ6QvUFa8Vp3 zdqnUKY$E%+|*`WDn75 zvh8TyPg>27u>AFd>ln}mR0l?NA@6=C^&^AR`MmD(YTJejAq0-08=eIwchPSMBO(4Lzv_g(x$#M6b)Ye>zncuZg1wz+_jR5Cc}gk^GCGjTJ8$nsmO=9 z2LwctxPVG4P`-#Tc82FP$)3ZN7$x^KozI>ie%BMfx4k1>X5&(ua zl+@H150$?ESXd0q)Y7qOzZi5cwbI_8scaTLM|bqpAnyH6Ep~+l|GyyNl<06S@^tOt z8n*Qs>z(d+^cxFXgtupTxoV1Ffh0BS?F-NbMZh#Sjb8;Puo?dD(S2(y+iq_5c42(4t*L9lE z?>!q&Wl^WotQEECygxA1(ZBuX{ct3nJ?uzi;O_)Xruc?#@9>S)BAy#ybGJZnBMKh8 zT8Q5pcmum=JMXLXV?d~;Ci6)*Wr>*`!xGr z>*_XoXOWM=e-D{J9MCyw)oslsqLtMQ%O7n9Rze&jWbbrR(7?U3!4ER!ASgwUH#rX} z8n-&TxxXR8t?XujhT(9gva>d`QwFTejM&TB><85U$Jbj&RsF4N-;|Uz64FRFNOuS# zQX<_Y-QAs%3KD{VfPk>*?(XjHmhO&c@^hc_p7T6=zyGX(YdFRlYp(g7^N#DjKEOXl z^}an&2B9N{k7U}eYkxwP+bUnBu$-GmYYXEJozL`b*X?8>T7L?^s!ij~VSL^km@Xfe zqDlEB$Gh)PjdgBhSlc*hSOqluXy3vbKE?@$@|{W-1^S_9T@^N!-qNoGy9eN>$}l& z$KGG{-7LM|%IVy*iPoGl4l4tHH&ByuXy2D=hJZ$Kmz(O)hlo^1sAiGplBBT0P;)Pl zUP8tGURs64Y@aRp!sxY{=t|<1v94nz?H^RV{eO$@!vuf>w{Lk35=m+@(lYdO_ zJ1|@~zq^%$sTS!J01IsetM@TYk%oHszomhDW|?`A`ssvKwWz@~i|N8Ee+y{U)hu+Z z%JB4pnLUyH2`0UO4>{>qs}3~jWs6`Bz-w2B&JUNAp<)cOy@KI`2?6yKwvxA2Rbg%% z?-U;Lh3FW%@;3-JEL^-IXARw#Uin&F`ys4usulYaMhB?QfPqxz6zO<#6CfD zhmaa67?m*ois1FH<2L>$dQc?3*Zuia)p&FUvM?C^y5aLA^CXu6K7*jhL_OT&G4M3T zm+HFud73W8uJfhwYwUafFHxTdpImZF4wnI^D;JzHyefs=&A|A?+i64sFBjmFREdEp z%Fan2i>f4%o%mPT(t|LJ?e(q9SBTT7PX&@~2RMUSrQzENh%S=&sp(^ukc+_{E0QV9 z$~iIO;#+U0)pVC&HxxG0vM-Z%du(RZfj@r(@pv|*x^*7ve6e2ysup&<=Ocv(0*t%o zmme@M8$RkpaqfT0?;4^$zdi=$Wr+Q$f|7HOIs^egm|oY>s*GY;`zPDou)L?F@VeE9 zu_PKn=yql(NeWs9<%-4fMV0HZ!TE9kdLB?nX?Ov|;|JipD?9L#VhDg7ai3@~cmwUuBM;?_e?|qu=L0#Ho@c4RJIPW3#iyz!AX|T5nr? z^`53v`Hkm=e2YW7F>FSKcJHryV-nSk!B-GrkzOykgIOUcQxu=$&eynMLPnqjtyR}M zKPW?{0}tixVmt3T746w*@$FzM!5Jg1Dqj~5j266YUfOd2_p$qFcZt~y15OiIpst%T z*JEk(gZUgoJ6Nh>aB=5>TVNm`CJ+(!ltES7DGtzuQVbA_9XM2Y#Uj%G_JU|bnyso9 zaBo{`CsZ_ZpLmbCk%mrHO?kJSgf=n*LjX$h_h9t%lhOmz^vF%mVu{2Z5_P>ca0)r( zfrZZ}m_ppoMBY3#N~a}e?lh~J z@|qx~H+sKLMEL^oPI$^*yw-b7OC7-M%V;fxeONys?NCbP9^3m~80$G$!`B$^uo7=u zckFp9kgzKu*K5sbP+#g{_Yv8dBuTx>YXOXHVs7!Ga{AAjH;hNQ>si|01mdSF3>dx( z<9uz{iL!+t!bpcg+~&1= zAAoxrf5O;Ke-{Q87(3vQGi8kW*RmTRT2`+FmxOgiS=~`i;c8l9GL-dDC%6C4uKd0n_26NSOZ<>qjcvbnv6b&7Qrwy_q zeInQ6?)mYukI(CdgVS+)SWRmJmj6&d7FZM!BRkix9PtJ*ox-Af8cx*dXgt~u@jTw0 zK7lQW=4^rW$8E~ZNkUUT;Eg5KuuOkL+tdP(M~IH+v4o)kl(A=L#LG-z2jp@N%z)IN z!V=V7kGtcpiLxOf!c7l=sC^?WfC#JxeWE+ai$MrVr|X*6^NV73V(3@x*s9*aCYZPR z7%g=3Tc_hVwDni})$!WIFR9pZ;EYzF17hnOoBAXd| zF5-pXwcdODz5}+}Rlwb+Gc_M*p=#V>#7~w_{f0j80+@3;Yic!@$>5LN5AahP>pihJ zDtv3x%ptqjX|Rbwtrin`%A4*jCct&d`9lbayJ_L!_qSUihoxXEpzoxIki6d4KYyX3 zcE3&|%(nl+tEi_JmAXvJ1>hg5Tf`rMmEVi-C16l)xRL>*hm8Gt6&{@=zey1tK;&AQ4Vvi7(u869B%S}<47k_Th#b5 z#|}l<$*ID;i(N{(KGr`)cadbIW$q<3eoLG7Q#@TQCB-^zGa<#5%hO;&>kqMmh*(ER zFVY2XoND_y%TZhDExP_56c z%u^6RxP_h(xDhHNYNy%kcFBU6tO~ECVT;UhRBQEq@)sASY?S)RI~|f+Vd&}C*k9EL zoi7*GAQe=Ny^?kG-g&svBxrS?9X#3yF9xW31A}gJ5e(Kl@O1NQ>P1D{F7G>3z5Hn))4;##7Zvf7H%i`zH7r2We5ShN~bz!Yi5)C4y-99;sJ=G|v z2Yp~IEO!t+W--{4?aq!BQvS;e;7O}pop1+Wxm<)2lMvVpn!#OQXf8efW12_U;};j!{{0 zREdT$DANs`R3mMm0EODn+cZ^06 z8)h%!*)~OF{PoLE1R0)YKSQdv8)K{UHo!u&?}6EA0wQ_xsVI*3mH}GpxmwVEEfRL8 z39j9|z4Fnmv3iWkkZO8b%0ZjZ?OrCD@ZFIbcX4lhs%DLS*3FaIeEa9O2E02hdCgA* z_Sy0wjK~4yQaivVGnWBKb4pr4=o-x-puh}aSepkP@Wey{%0xJqIR_>p-| zyOUB%uI=kMsq4*(^sB5hyb48y2hY)BIj-`jZU;qXMG=U96-!m{gq~ou%JZxQDwPnq z9dq}ePwCpEnzsVSRvvGmr1Z|$e@n++eo!3bjI0Fg#!3b*#~gnsqWd|T=CGbZBMr(g zzog%532J{l=`Nuz%5<+YixJ+boHQvIg%~aV{G!O!tIDJcDo_Jm+v_cLDh=nPtG>a7 zV$4-BH7HP)OWn<6a;unIoteP`a=n{$vEQn+&nYJcsvq9yUF_AE_rzQwf7Eq6kT!X) zh`H9;vEOoYF>^b5{bEKnG_J61rw)NMLO%7~4KOB;_#LF2DRqgPs3TRD`y6xsYAss6 zKy&u$aX$rVno`28%R`telzGa&x0JDN-oBe0DS_C_`_tSrmt=iT_pm1 z!(LJQx>c`p#dK5X;A0|^kzLv|_t`W1`-aUI-^%XjPS{*(axSnhoDL|kpMt>(2xKv@ z@J9J^@A4jUQ(#SmFf%v=9MO87;b|>9!9FqvHilF{yNZ2c!Rjq1UcKKY;0U>1FSo3&mP^AWTfj0mcI(&a8l zunK@+%ZP15u^oeueo9K#4l!FT1p+I+Sx`Cqo+hr7ypsfkN3zWN%l6N230^##k0!kt z4v7JoK@Dvks}(SW?Lp(;(_7%cc_}i_Qb(fl!7u|!Ub@B!G}eyo~q-jC+np_M=D zB&2(a%chvC^IGBB!5#H2%h2cs_;CfBpaWo~jb zJzwzkK)mFoHs6I4B6TtyiT@|E*270yKE$L+D$<_z#2nCdO6GertbrR&Ng zHoJ6DSMXNQa1|{1YN%K@9%j7WA}6G-EjK(MS72*hF~>+Z`_9wft+sn;K#2Bcl1h4) z4iP0HAB*Eov+(udC-EffsGl*;*Rxu4JJpr?o}P&Fo@IyFE?IFZ972|T3tA|!-$8iX zX|Xvb|F~F9t z!6R+YVNnD(Gkg1ayDoLoO^lEuzwLC`K`MFDa22(S(v(_ReWKP&KqIk#_%a%Z)IFuV#aEE>SZkTM$SsyA)<$d)+uPyv;zz*kb|o(A@z1 z*4OQf^|?P`Z~Sn!jG!M?3E6)Ckr>qLjLYW5l`VQ8^1$gB1^TTa@sLWfcB8B7lrZ} z?w+#d)oAFpwfX!<=ecN4~Q=1U_w_7aVDrpsMur zpmi^lVO8LFkzUBW_XLpW@&mo2O_;^?=2I$$8Zb=&hZKz?GbY2B;yJMKhyl=C`==vujZ5iv*o`cYWLIJiZzxRq(y zpD6d&9D>&`l53WX&LQ@X_$mLsH+zb%A63YADDO+jFNP6b$7g*?8F3s$RH+r(eYM+! zxRS}^gOK#}39t3)5Cp!~`KEo_cHk4nvj#m#yrPMv+;O2|8{ib_+#C zmOmUOT?m)>PdN@V#~M9)r6NS*6J3 z+Q3yPNwed{KN+uIE>`v9Lq{a2B~%_c+@Cj@;a>(SHna~Xkm6~}+?aXeX-IfKhMk8t z1@tefqn>WM8x~YdS`yuE3=s}oI6_9mVkbyVD^1eu%-1qb!De`9$IAS}T$Xg8too2) zH{OM2QST64#lnnLwjvjGpB8V%8>j-Exdk672mREBjfVRHsfsL6Uv!Sz{R0ILxww+eDXZT z+qo^cbpT7tISAT1ZMpFG#3kLlq}Oq{?pfGsfj>QSp(D8vX%hU}->@Z9PA>p;ld8

2Yv4M9Mw5qmJ~_!n&|RI!U8;L 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

*V>aC?>WQ^{-fZe~_sidY7E%8JWE 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�@)`?Gfh284R=I}2W-56R6i`!Ufpx& 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%0D;%_T`lfQQi=k9S~Fr_TBzLh4W3svFz=J@#@ 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

    %$3$9f28TaX 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

    Ac|UPB(jipKAiKR@~Hv+PYeN5vvB*?W>4 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%0A-&EjDkNG{AyX{~vomgn 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ξ&e6h=RqD6<4kt`ngX$ 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 OK;Y@>=d#Wzp$PyMe`>=3 literal 0 HcmV?d00001 diff --git a/blog/images/20220901-incognito3.png b/blog/images/20220901-incognito3.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6c0db6cc2432182ee84492ffdf18f041c5f682 GIT binary patch literal 300876 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

    w0oO}>1TRjx_=34~24~*e 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

    qHI^NX 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�SL?&I$`!P=g@@@bvM$x`+l4wu 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

    BSy~6V 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 literal 0 HcmV?d00001 diff --git a/blog/images/20220901-onion1.png b/blog/images/20220901-onion1.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab4cc5a4d7e421331b0470d6e1d39039d852bbb GIT binary patch literal 444925 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⪙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