From f82fa42cba0740ee596640af93fc2ee52c074ba0 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Wed, 26 Apr 2023 18:37:13 +0300 Subject: [PATCH 01/22] github: issue templates (#2330) * issue template test * templates: allow blank issues --- .github/ISSUE_TEMPLATE/bug.yml | 68 +++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature.yml | 40 +++++++++++++++++ .github/ISSUE_TEMPLATE/question.yml | 16 +++++++ 4 files changed, 125 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..a6ffb6d328 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,68 @@ +name: Bug +description: File a bug report/issue +title: "[Bug]: " +labels: ["bug", "triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: dropdown + attributes: + label: Platform + description: Multiple selections are possible. + multiple: true + options: + - Linux + - Mac + - Windows + - Android + - iOS + validations: + required: true +- type: input + attributes: + label: OS version + description: Specify the OS version + placeholder: ex. Android 12, Ubuntu 20.04 + validations: + required: true +- type: input + attributes: + label: App version + description: Specify the SimpleX version + placeholder: ex. 4.3.2 + validations: + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + placeholder: Bug happened! + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + placeholder: No bug should happen! + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to ... + 3. Click on ... + 4. See error... + validations: + required: true +- type: textarea + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..0086358db1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000000..936d1dcaf5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,40 @@ +name: Feature +description: Suggest your feature +title: "[Feature]: " +labels: ["enhancement", "triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: dropdown + attributes: + label: Platform + description: Multiple selections are possible. If selected input is "all", this considered to be a general feature. + multiple: true + options: + - Linux + - Mac + - Windows + - Android + - iOS + - all + validations: + required: true +- type: input + attributes: + label: App version + description: Specify the SimpleX version + placeholder: ex. 4.3.2 + validations: + required: false +- type: textarea + attributes: + label: Feature + description: Describe the feature you would like to see added + placeholder: SimpleX Chat should make me coffee! + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000000..8506c69e0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,16 @@ +name: Question +description: Ask your question +title: "[Q]: " +labels: ["question", "triage"] +body: +- type: markdown + attributes: + value: | + Generally, we encourage you to ask questions in our [official group](https://simplex.chat/invitation/#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3Dsimplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D), but you can do it anyway :) +- type: textarea + attributes: + label: Question + description: Please ask your question in plain english. + placeholder: Is SimpleX - chat? + validations: + required: true From 591aa9eaa51a9c3461c15bd2de9dfa75fa9ca85c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 27 Apr 2023 09:12:34 +0200 Subject: [PATCH 02/22] core: get all chat items API (#2333) * core: get all chat items API * test --- src/Simplex/Chat.hs | 6 +++-- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Messages.hs | 3 +++ src/Simplex/Chat/Store.hs | 47 +++++++++++++++++++++++++--------- tests/ChatTests/Direct.hs | 23 +++++++++++++++-- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 12e7c63455..e241e0357f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -453,7 +453,9 @@ processChatCommand = \case pure $ CRApiChat user (AChat SCTGroup groupChat) CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIGetChatItems _pagination -> pure $ chatCmdError Nothing "not implemented" + APIGetChatItems pagination search -> withUser $ \user -> do + chatItems <- withStore $ \db -> getAllChatItems db user pagination search + pure $ CRChatItems user chatItems APISendMessage (ChatRef cType chatId) live (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of CTDirect -> do ct@Contact {contactId, localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId @@ -4565,7 +4567,7 @@ chatCommandP = "/sql agent " *> (ExecAgentStoreSQL <$> textP), "/_get chats " *> (APIGetChats <$> A.decimal <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), - "/_get items count=" *> (APIGetChatItems <$> A.decimal), + "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), "/_send " *> (APISendMessage <$> chatRefP <*> liveMessageP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP), "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 03f04166ed..bd0299d78f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -212,7 +212,7 @@ data ChatCommand | ExecAgentStoreSQL Text | APIGetChats {userId :: UserId, pendingConnections :: Bool} | APIGetChat ChatRef ChatPagination (Maybe String) - | APIGetChatItems Int + | APIGetChatItems ChatPagination (Maybe String) | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, composedMessage :: ComposedMessage} | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent} | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 7ef3534cbe..e32fa2b00d 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -299,6 +299,9 @@ aChatItems (AChat ct Chat {chatInfo, chatItems}) = map aChatItem chatItems aChatItemId :: AChatItem -> Int64 aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci +aChatItemTs :: AChatItem -> UTCTime +aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci + updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d updateFileStatus ci@ChatItem {file} status = case file of Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 051a04450f..f494c910ba 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -4241,18 +4241,17 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) getAllChatItems :: DB.Connection -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] -getAllChatItems db user pagination search_ = do - let search = fromMaybe "" search_ - case pagination of - CPLast count -> getAllChatItemsLast_ db user count search - CPAfter _afterId _count -> throwError $ SEInternalError "not implemented" - CPBefore _beforeId _count -> throwError $ SEInternalError "not implemented" - -getAllChatItemsLast_ :: DB.Connection -> User -> Int -> String -> ExceptT StoreError IO [AChatItem] -getAllChatItemsLast_ db user@User {userId} count search = do +getAllChatItems db user@User {userId} pagination search_ = do itemRefs <- - liftIO $ - reverse . rights . map toChatItemRef + rights . map toChatItemRef <$> case pagination of + CPLast count -> liftIO $ getAllChatItemsLast_ count + CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem db user afterId + CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem db user beforeId + mapM (uncurry $ getAChatItem_ db user) itemRefs + where + search = fromMaybe "" search_ + getAllChatItemsLast_ count = + reverse <$> DB.query db [sql| @@ -4263,7 +4262,31 @@ getAllChatItemsLast_ db user@User {userId} count search = do LIMIT ? |] (userId, search, count) - mapM (uncurry $ getAChatItem_ db user) itemRefs + getAllChatItemsAfter_ afterId count afterTs = + DB.query + db + [sql| + SELECT chat_item_id, contact_id, group_id + FROM chat_items + WHERE user_id = ? 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, search, afterTs, afterTs, afterId, count) + getAllChatItemsBefore_ beforeId count beforeTs = + reverse + <$> DB.query + db + [sql| + SELECT chat_item_id, contact_id, group_id + FROM chat_items + WHERE user_id = ? 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, search, beforeTs, beforeTs, beforeId, count) getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId getGroupIdByName db User {userId} gName = diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index e4d120d1bc..e54bddb386 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1544,7 +1544,9 @@ testUserPrivacy = showActiveUser alice "alisa" -- connect using second user connectUsers alice bob + threadDelay 1000000 alice #> "@bob hello" + threadDelay 1000000 bob <# "alisa> hello" bob #> "@alisa hey" alice <# "bob> hey" @@ -1576,12 +1578,19 @@ testUserPrivacy = alice <## "alisa (active, hidden, muted)" -- hidden message is saved alice ##> "/tail" + alice <##? chatHistory + alice ##> "/_get items count=10" + alice <##? chatHistory + alice ##> "/_get items before=9 count=10" alice <##? [ "bob> Disappearing messages: allowed", "bob> Full deletion: off", "bob> Voice messages: enabled", - "bob> Audio/video calls: enabled", - "@bob hello", + "bob> Audio/video calls: enabled" + ] + alice ##> "/_get items after=8 count=10" + alice + <##? [ "@bob hello", "bob> hey", "bob> hello again", "bob> this won't show" @@ -1634,6 +1643,16 @@ testUserPrivacy = alice <## (current <> "user alisa:") alice <## "messages are shown" alice <## "profile is visible" + chatHistory = + [ "bob> Disappearing messages: allowed", + "bob> Full deletion: off", + "bob> Voice messages: enabled", + "bob> Audio/video calls: enabled", + "@bob hello", + "bob> hey", + "bob> hello again", + "bob> this won't show" + ] testSetChatItemTTL :: HasCallStack => FilePath -> IO () testSetChatItemTTL = From 8630d1ab123f288ef3a4ad7897ff58ff21abab26 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:18:40 +0300 Subject: [PATCH 03/22] templates: update labels (#2335) --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/feature.yml | 2 +- .github/ISSUE_TEMPLATE/question.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a6ffb6d328..6256823471 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,7 +1,7 @@ name: Bug description: File a bug report/issue title: "[Bug]: " -labels: ["bug", "triage"] +labels: ["type:bug", "type:triage"] body: - type: checkboxes attributes: diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 936d1dcaf5..84b8ac5dcc 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,7 +1,7 @@ name: Feature description: Suggest your feature title: "[Feature]: " -labels: ["enhancement", "triage"] +labels: ["type:enhancement", "type:triage"] body: - type: checkboxes attributes: diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 8506c69e0a..b69aa7a3ab 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,7 +1,7 @@ name: Question description: Ask your question title: "[Q]: " -labels: ["question", "triage"] +labels: ["type:question", "type:triage"] body: - type: markdown attributes: From 0b57cc08a75ef47ba263ac3f53b3e6d8f2bb86ec Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 27 Apr 2023 17:19:21 +0400 Subject: [PATCH 04/22] core, ios: include contact addresses in profiles (#2328) * core: include contact links in profiles * add connection request link to contact and group profiles * set group link on update, view, api * core: include contact addresses in profiles * remove id from UserContactLink * schema, fix test * remove address from profile when deleting link, tests * remove diff * remove diff * fix * ios wip * learn more, confirm save, reset on delete * re-use in create link view * remove obsolete files * color * revert scheme * learn more with create * layout * layout * progress indicator * delete text * save on change, layout --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 14 +- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 15 + .../Chat/Group/GroupMemberInfoView.swift | 15 + .../Shared/Views/NewChat/CreateLinkView.swift | 2 +- .../UserSettings/AcceptRequestsView.swift | 131 ------ .../Views/UserSettings/SettingsView.swift | 7 +- .../Views/UserSettings/UserAddress.swift | 128 ------ .../UserSettings/UserAddressLearnMore.swift | 29 ++ .../Views/UserSettings/UserAddressView.swift | 396 ++++++++++++++++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/APITypes.swift | 3 + apps/ios/SimpleXChat/ChatTypes.swift | 32 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 32 +- src/Simplex/Chat/Controller.hs | 2 + .../M20230422_profile_contact_links.hs | 18 + src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/ProfileGenerator.hs | 2 +- src/Simplex/Chat/Store.hs | 168 ++++---- src/Simplex/Chat/Types.hs | 10 +- src/Simplex/Chat/View.hs | 18 +- tests/ChatTests/Profiles.hs | 78 ++++ tests/ChatTests/Utils.hs | 8 +- tests/ProtocolTests.hs | 4 +- 24 files changed, 757 insertions(+), 375 deletions(-) delete mode 100644 apps/ios/Shared/Views/UserSettings/AcceptRequestsView.swift delete mode 100644 apps/ios/Shared/Views/UserSettings/UserAddress.swift create mode 100644 apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift create mode 100644 apps/ios/Shared/Views/UserSettings/UserAddressView.swift create mode 100644 src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 53bb7db2d6..3ecfa70793 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -618,6 +618,16 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? { } } +func apiSetProfileAddress(on: Bool) async throws -> User? { + let userId = try currentUserId("apiSetProfileAddress") + let r = await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) + switch r { + case .userProfileNoChange: return nil + case let .userProfileUpdated(user, _, _): return user + default: throw r + } +} + func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? { let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact } @@ -643,10 +653,10 @@ func apiCreateUserAddress() async throws -> String { throw r } -func apiDeleteUserAddress() async throws { +func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId)) - if case .userContactLinkDeleted = r { return } + if case let .userContactLinkDeleted(user) = r { return user } throw r } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 5cab211e89..b72006b3fe 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -116,6 +116,21 @@ struct ChatInfoView: View { contactPreferencesButton() } + if let contactLink = contact.contactLink { + Section { + QRCode(uri: contactLink) + Button { + showShareSheet(items: [contactLink]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") + } + } header: { + Text("Address") + } footer: { + Text("You can share this address with your contacts to let them connect with **\(contact.displayName)**.") + } + } + Section("Servers") { networkStatusRow() .onTapGesture { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 9f367fb472..dd13741141 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -65,6 +65,21 @@ struct GroupMemberInfoView: View { } } + if let contactLink = member.contactLink { + Section { + QRCode(uri: contactLink) + Button { + showShareSheet(items: [contactLink]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") + } + } header: { + Text("Address") + } footer: { + Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") + } + } + Section("Member") { infoRow("Group", groupInfo.displayName) diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift index 4504361bd7..f1274ef388 100644 --- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift +++ b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift @@ -49,7 +49,7 @@ struct CreateLinkView: View { ) } .tag(CreateLinkTab.oneTime) - UserAddress() + UserAddressView(viaCreateLinkView: true) .tabItem { Label("Your contact address", systemImage: "infinity.circle") } diff --git a/apps/ios/Shared/Views/UserSettings/AcceptRequestsView.swift b/apps/ios/Shared/Views/UserSettings/AcceptRequestsView.swift deleted file mode 100644 index 0e61658c93..0000000000 --- a/apps/ios/Shared/Views/UserSettings/AcceptRequestsView.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// AcceptRequestsView.swift -// SimpleX (iOS) -// -// Created by Evgeny on 23/10/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -struct AcceptRequestsView: View { - @EnvironmentObject private var m: ChatModel - @State var contactLink: UserContactLink - @State private var a = AutoAcceptState() - @State private var saved = AutoAcceptState() - @FocusState private var keyboardVisible: Bool - - var body: some View { - List { - Section { - settingsRow("checkmark") { - Toggle("Automatically", isOn: $a.enable) - } - if a.enable { - settingsRow( - a.incognito ? "theatermasks.fill" : "theatermasks", - color: a.incognito ? .indigo : .secondary - ) { - Toggle("Incognito", isOn: $a.incognito) - } - } - } header: { - Text("Accept requests") - } footer: { - saveButtons() - } - if a.enable { - Section { - TextEditor(text: $a.welcomeText) - .focused($keyboardVisible) - .padding(.horizontal, -5) - .padding(.top, -8) - .frame(height: 90, alignment: .topLeading) - .frame(maxWidth: .infinity, alignment: .leading) - } header: { - Text("Welcome message") - } - } - } - .onAppear { - a = AutoAcceptState(contactLink: contactLink) - saved = a - } - .onChange(of: a.enable) { _ in - if !a.enable { a = AutoAcceptState() } - } - } - - @ViewBuilder private func saveButtons() -> some View { - HStack { - Button { - a = saved - } label: { - Label("Cancel", systemImage: "arrow.counterclockwise") - } - Spacer() - Button { - Task { - do { - if let link = try await userAddressAutoAccept(a.autoAccept) { - contactLink = link - m.userAddress = link - saved = a - } - } catch let error { - logger.error("userAddressAutoAccept error: \(responseError(error))") - } - } - } label: { - Label("Save", systemImage: "checkmark") - } - } - .font(.body) - .disabled(a == saved) - } - - private struct AutoAcceptState: Equatable { - var enable = false - var incognito = false - var welcomeText = "" - - init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { - self.enable = enable - self.incognito = incognito - self.welcomeText = welcomeText - } - - init(contactLink: UserContactLink) { - if let aa = contactLink.autoAccept { - enable = true - incognito = aa.acceptIncognito - if let msg = aa.autoReply { - welcomeText = msg.text - } else { - welcomeText = "" - } - } else { - enable = false - incognito = false - welcomeText = "" - } - } - - var autoAccept: AutoAccept? { - if enable { - var autoReply: MsgContent? = nil - let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) - if s != "" { autoReply = .text(s) } - return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) - } - return nil - } - } -} - -struct AcceptRequestsView_Previews: PreviewProvider { - static var previews: some View { - AcceptRequestsView(contactLink: UserContactLink(connReqContact: "")) - } -} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index d25ea45f49..4f8bb4f503 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -147,10 +147,11 @@ struct SettingsView: View { incognitoRow() NavigationLink { - CreateLinkView(selection: .longTerm, viaNavLink: true) - .navigationBarTitleDisplayMode(.inline) + UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("qrcode") { Text("Your SimpleX contact address") } + settingsRow("qrcode") { Text("Your SimpleX address") } } NavigationLink { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/UserSettings/UserAddress.swift deleted file mode 100644 index 7564bea358..0000000000 --- a/apps/ios/Shared/Views/UserSettings/UserAddress.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// UserAddress.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 31/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -struct UserAddress: View { - @EnvironmentObject private var chatModel: ChatModel - @State private var alert: UserAddressAlert? - @State private var showAcceptRequests = false - - private enum UserAddressAlert: Identifiable { - case deleteAddress - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") - - var id: String { - switch self { - case .deleteAddress: return "deleteAddress" - case let .error(title, _): return "error \(title)" - } - } - } - - var body: some View { - ScrollView { - VStack (alignment: .leading) { - Text("You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.") - .padding(.bottom) - if let userAdress = chatModel.userAddress { - QRCode(uri: userAdress.connReqContact) - HStack { - Button { - showShareSheet(items: [userAdress.connReqContact]) - } label: { - HStack { - Image(systemName: "square.and.arrow.up") - Text("Share link") - } - } - .padding() - NavigationLink { - if let contactLink = chatModel.userAddress { - AcceptRequestsView(contactLink: contactLink) - .navigationTitle("Contact requests") - .navigationBarTitleDisplayMode(.large) - } - } label: { - HStack { - Text("Contact requests") - Image(systemName: "chevron.right") - } - } - .padding() - } - .frame(maxWidth: .infinity) - Button(role: .destructive) { alert = .deleteAddress } label: { - Label("Delete address", systemImage: "trash") - } - .frame(maxWidth: .infinity) - } else { - Button { - Task { - do { - let connReqContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connReqContact: connReqContact) - } - } catch let error { - logger.error("UserAddress apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - } - } - } label: { Label("Create address", systemImage: "qrcode") } - .frame(maxWidth: .infinity) - } - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .sheet(isPresented: $showAcceptRequests) { - if let contactLink = chatModel.userAddress { - AcceptRequestsView(contactLink: contactLink) - } - } - .alert(item: $alert) { alert in - switch alert { - case .deleteAddress: - return Alert( - title: Text("Delete address?"), - message: Text("All your contacts will remain connected"), - primaryButton: .destructive(Text("Delete")) { - Task { - do { - try await apiDeleteUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = nil - } - } catch let error { - logger.error("UserAddress apiDeleteUserAddress: \(responseError(error))") - } - } - }, secondaryButton: .cancel() - ) - case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) - } - } - } - } -} - -struct UserAddress_Previews: PreviewProvider { - static var previews: some View { - let chatModel = ChatModel() - chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") - return Group { - UserAddress() - .environmentObject(chatModel) - UserAddress() - .environmentObject(ChatModel()) - } - } -} diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift new file mode 100644 index 0000000000..df7bcf2767 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -0,0 +1,29 @@ +// +// UserAddressLearnMore.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 27.04.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct UserAddressLearnMore: View { + var body: some View { + List { + VStack(alignment: .leading, spacing: 18) { + Text("You can create a long term address that can be used by other people to connect with you.") + Text("Unlike 1-time invitation links, these addresses can be used many times, that makes them good to share online.") + Text("When people connect to you via this address, you will receive a connection request that you can accept or reject.") + Text("Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address).") + } + .listRowBackground(Color.clear) + } + } +} + +struct UserAddressLearnMore_Previews: PreviewProvider { + static var previews: some View { + UserAddressLearnMore() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift new file mode 100644 index 0000000000..1dbd466f50 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -0,0 +1,396 @@ +// +// UserAddressView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 26.04.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct UserAddressView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject private var chatModel: ChatModel + @State var viaCreateLinkView = false + @State var shareViaProfile = false + @State private var aas = AutoAcceptState() + @State private var savedAAS = AutoAcceptState() + @State private var ignoreShareViaProfileChange = false + @State private var alert: UserAddressAlert? + @State private var showSaveDialogue = false + @State private var progressIndicator = false + @FocusState private var keyboardVisible: Bool + + private enum UserAddressAlert: Identifiable { + case deleteAddress + case profileAddress(on: Bool) + case shareOnCreate + case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + + var id: String { + switch self { + case .deleteAddress: return "deleteAddress" + case let .profileAddress(on): return "profileAddress \(on)" + case .shareOnCreate: return "shareOnCreate" + case let .error(title, _): return "error \(title)" + } + } + } + + var body: some View { + ZStack { + if viaCreateLinkView { + userAddressView() + } else { + userAddressView() + .modifier(BackButton { + if savedAAS == aas { + dismiss() + } else { + showSaveDialogue = true + } + }) + .confirmationDialog("Save settings?", isPresented: $showSaveDialogue) { + Button("Save auto-accept settings") { + saveAAS() + dismiss() + } + Button("Exit without saving") { dismiss() } + } + } + if progressIndicator { + ZStack { + if chatModel.userAddress != nil { + Circle() + .fill(.white) + .opacity(0.7) + .frame(width: 56, height: 56) + } + ProgressView().scaleEffect(2) + } + } + } + } + + @ViewBuilder private func userAddressView() -> some View { + List { + if let userAddress = chatModel.userAddress { + existingAddressView(userAddress) + .onAppear { + aas = AutoAcceptState(userAddress: userAddress) + savedAAS = aas + } + .onChange(of: aas.enable) { _ in + if !aas.enable { aas = AutoAcceptState() } + } + } else { + Section { + createAddressButton() + } footer: { + Text("Create an address to let people connect with you.") + } + + Section { + learnMoreButton() + } + } + } + .alert(item: $alert) { alert in + switch alert { + case .deleteAddress: + return Alert( + title: Text("Delete address?"), + message: + shareViaProfile + ? Text("All your contacts will remain connected. Profile update will be sent to your contacts.") + : Text("All your contacts will remain connected."), + primaryButton: .destructive(Text("Delete")) { + progressIndicator = true + Task { + do { + if let u = try await apiDeleteUserAddress() { + DispatchQueue.main.async { + chatModel.userAddress = nil + chatModel.updateUser(u) + if shareViaProfile { + ignoreShareViaProfileChange = true + shareViaProfile = false + } + } + } + await MainActor.run { progressIndicator = false } + } catch let error { + logger.error("UserAddressView apiDeleteUserAddress: \(responseError(error))") + await MainActor.run { progressIndicator = false } + } + } + }, secondaryButton: .cancel() + ) + case let .profileAddress(on): + if on { + return Alert( + title: Text("Share address with contacts?"), + message: Text("Profile update will be sent to your contacts."), + primaryButton: .default(Text("Share")) { + setProfileAddress(on) + }, secondaryButton: .cancel() { + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ) + } else { + return Alert( + title: Text("Stop sharing address?"), + message: Text("Profile update will be sent to your contacts."), + primaryButton: .default(Text("Stop sharing")) { + setProfileAddress(on) + }, secondaryButton: .cancel() { + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ) + } + case .shareOnCreate: + return Alert( + title: Text("Share address with contacts?"), + message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."), + primaryButton: .default(Text("Share")) { + setProfileAddress(true) + ignoreShareViaProfileChange = true + shareViaProfile = true + }, secondaryButton: .cancel() + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + } + + @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { + Section { + QRCode(uri: userAddress.connReqContact) + shareQRCodeButton(userAddress) + shareWithContactsButton() + autoAcceptToggle() + learnMoreButton() + } header: { + Text("Address") + } + + if aas.enable { + autoAcceptSection() + } + + Section { + deleteAddressButton() + } footer: { + Text("Your contacts will remain connected.") + } + } + + private func createAddressButton() -> some View { + Button { + progressIndicator = true + Task { + do { + let connReqContact = try await apiCreateUserAddress() + DispatchQueue.main.async { + chatModel.userAddress = UserContactLink(connReqContact: connReqContact) + alert = .shareOnCreate + progressIndicator = false + } + } catch let error { + logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") + let a = getErrorAlert(error, "Error creating address") + alert = .error(title: a.title, error: a.message) + await MainActor.run { progressIndicator = false } + } + } + } label: { + Label("Create SimpleX address", systemImage: "qrcode") + } + } + + private func deleteAddressButton() -> some View { + Button(role: .destructive) { + alert = .deleteAddress + } label: { + Label("Delete address", systemImage: "trash") + .foregroundColor(Color.red) + } + } + + private func shareQRCodeButton(_ userAdress: UserContactLink) -> some View { + Button { + showShareSheet(items: [userAdress.connReqContact]) + } label: { + settingsRow("square.and.arrow.up") { + Text("Share address") + } + } + } + + private func autoAcceptToggle() -> some View { + settingsRow("checkmark") { + Toggle("Auto-accept", isOn: $aas.enable) + .onChange(of: aas.enable) { _ in + saveAAS() + } + } + } + + private func learnMoreButton() -> some View { + NavigationLink { + UserAddressLearnMore() + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + } label: { + settingsRow("info.circle") { + Text("Learn more") + } + } + } + + private func shareWithContactsButton() -> some View { + settingsRow("person") { + Toggle("Share with contacts", isOn: $shareViaProfile) + .onChange(of: shareViaProfile) { on in + if ignoreShareViaProfileChange { + ignoreShareViaProfileChange = false + } else { + alert = .profileAddress(on: on) + } + } + } + } + + private func setProfileAddress(_ on: Bool) { + progressIndicator = true + Task { + do { + if let u = try await apiSetProfileAddress(on: on) { + DispatchQueue.main.async { + chatModel.updateUser(u) + } + } + await MainActor.run { progressIndicator = false } + } catch let error { + logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))") + await MainActor.run { progressIndicator = false } + } + } + } + + private struct AutoAcceptState: Equatable { + var enable = false + var incognito = false + var welcomeText = "" + + init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { + self.enable = enable + self.incognito = incognito + self.welcomeText = welcomeText + } + + init(userAddress: UserContactLink) { + if let aa = userAddress.autoAccept { + enable = true + incognito = aa.acceptIncognito + if let msg = aa.autoReply { + welcomeText = msg.text + } else { + welcomeText = "" + } + } else { + enable = false + incognito = false + welcomeText = "" + } + } + + var autoAccept: AutoAccept? { + if enable { + var autoReply: MsgContent? = nil + let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) + if s != "" { autoReply = .text(s) } + return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) + } + return nil + } + } + + @ViewBuilder private func autoAcceptSection() -> some View { + Section { + acceptIncognitoToggle() + welcomeMessageEditor() + saveAASButton() + .disabled(aas == savedAAS) + } header: { + Text("Accept requests") + } + } + + private func acceptIncognitoToggle() -> some View { + settingsRow( + aas.incognito ? "theatermasks.fill" : "theatermasks", + color: aas.incognito ? .indigo : .secondary + ) { + Toggle("Accept incognito", isOn: $aas.incognito) + } + } + + private func welcomeMessageEditor() -> some View { + ZStack { + if aas.welcomeText.isEmpty { + TextEditor(text: Binding.constant("Enter welcome message… (optional)")) + .foregroundColor(.secondary) + .disabled(true) + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 90, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) + } + TextEditor(text: $aas.welcomeText) + .focused($keyboardVisible) + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 90, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + private func saveAASButton() -> some View { + Button { + saveAAS() + } label: { + Text("Save") + } + } + + private func saveAAS() { + Task { + do { + if let address = try await userAddressAutoAccept(aas.autoAccept) { + chatModel.userAddress = address + savedAAS = aas + } + } catch let error { + logger.error("userAddressAutoAccept error: \(responseError(error))") + } + } + } +} + +struct UserAddressView_Previews: PreviewProvider { + static var previews: some View { + let chatModel = ChatModel() + chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") + return Group { + UserAddressView() + .environmentObject(chatModel) + UserAddressView() + .environmentObject(ChatModel()) + } + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8bed72bb71..2d8170bd79 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -104,7 +104,6 @@ 5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; - 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; }; 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; 5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */; }; @@ -116,7 +115,6 @@ 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; - 5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; @@ -172,6 +170,8 @@ 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; + 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; + 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; @@ -368,7 +368,6 @@ 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; - 5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = ""; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = ""; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 5CBD285629565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -384,7 +383,6 @@ 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; - 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptRequestsView.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; @@ -439,6 +437,8 @@ 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; + 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; + 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.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 = ""; }; @@ -709,8 +709,6 @@ 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, - 5CB924E327A8683A00ACCCDD /* UserAddress.swift */, - 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */, 5CB924E027A867BA00ACCCDD /* UserProfile.swift */, 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, @@ -722,6 +720,8 @@ 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, 5C65F341297D3F3600B67AF3 /* VersionView.swift */, 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */, + 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */, + 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */, ); path = UserSettings; sourceTree = ""; @@ -1069,7 +1069,6 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, - 5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */, 5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */, 5CC036E029C488D500C0EF20 /* HiddenProfileView.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, @@ -1089,7 +1088,6 @@ D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, - 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, @@ -1126,6 +1124,7 @@ 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */, + 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */, 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */, @@ -1137,6 +1136,7 @@ 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */, 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, + 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */, 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */, 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b1021c5130..53ae7e8849 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -85,6 +85,7 @@ public enum ChatCommand { case apiCreateMyAddress(userId: Int64) case apiDeleteMyAddress(userId: Int64) case apiShowMyAddress(userId: Int64) + case apiSetProfileAddress(userId: Int64, on: Bool) case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) case apiAcceptContact(contactReqId: Int64) case apiRejectContact(contactReqId: Int64) @@ -189,6 +190,7 @@ public enum ChatCommand { case let .apiCreateMyAddress(userId): return "/_address \(userId)" case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" @@ -290,6 +292,7 @@ public enum ChatCommand { case .apiCreateMyAddress: return "apiCreateMyAddress" case .apiDeleteMyAddress: return "apiDeleteMyAddress" case .apiShowMyAddress: return "apiShowMyAddress" + case .apiSetProfileAddress: return "apiSetProfileAddress" case .apiAddressAutoAccept: return "apiAddressAutoAccept" case .apiAcceptContact: return "apiAcceptContact" case .apiRejectContact: return "apiRejectContact" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a449af1c3c..f82bb844bc 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -33,6 +33,10 @@ public struct User: Decodable, NamedChat, Identifiable { activeUser || showNtfs } + public var addressShared: Bool { + profile.contactLink != nil + } + public static let sampleData = User( userId: 1, userContactId: 1, @@ -71,16 +75,24 @@ public typealias ContactName = String public typealias GroupName = String public struct Profile: Codable, NamedChat { - public init(displayName: String, fullName: String, image: String? = nil, preferences: Preferences? = nil) { + public init( + displayName: String, + fullName: String, + image: String? = nil, + contactLink: String? = nil, + preferences: Preferences? = nil + ) { self.displayName = displayName self.fullName = fullName self.image = image + self.contactLink = contactLink self.preferences = preferences } public var displayName: String public var fullName: String public var image: String? + public var contactLink: String? public var preferences: Preferences? public var localAlias: String { get { "" } } @@ -95,11 +107,20 @@ public struct Profile: Codable, NamedChat { } public struct LocalProfile: Codable, NamedChat { - public init(profileId: Int64, displayName: String, fullName: String, image: String? = nil, preferences: Preferences? = nil, localAlias: String) { + public init( + profileId: Int64, + displayName: String, + fullName: String, + image: String? = nil, + contactLink: String? = nil, + preferences: Preferences? = nil, + localAlias: String + ) { self.profileId = profileId self.displayName = displayName self.fullName = fullName self.image = image + self.contactLink = contactLink self.preferences = preferences self.localAlias = localAlias } @@ -108,6 +129,7 @@ public struct LocalProfile: Codable, NamedChat { public var displayName: String public var fullName: String public var image: String? + public var contactLink: String? public var preferences: Preferences? public var localAlias: String @@ -127,11 +149,11 @@ public struct LocalProfile: Codable, NamedChat { } public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile { - LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image, preferences: profile.preferences, localAlias: localAlias) + LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, localAlias: localAlias) } public func fromLocalProfile (_ profile: LocalProfile) -> Profile { - Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, preferences: profile.preferences) + Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) } public enum ChatType: String { @@ -1164,6 +1186,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } + public var contactLink: String? { get { profile.contactLink } } public var localAlias: String { profile.localAlias } public var verified: Bool { activeConn.connectionCode != nil } @@ -1508,6 +1531,7 @@ public struct GroupMember: Identifiable, Decodable { } public var fullName: String { get { memberProfile.fullName } } public var image: String? { get { memberProfile.image } } + public var contactLink: String? { get { memberProfile.contactLink } } public var verified: Bool { activeConn?.connectionCode != nil } var directChatId: ChatId? { diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7cfe9ead06..995f5c3cb6 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -92,6 +92,7 @@ library Simplex.Chat.Migrations.M20230402_protocol_servers Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions Simplex.Chat.Migrations.M20230420_rcv_files_to_receive + Simplex.Chat.Migrations.M20230422_profile_contact_links Simplex.Chat.Mobile Simplex.Chat.Mobile.WebRTC Simplex.Chat.Options diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e241e0357f..2cf37abb54 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1123,18 +1123,32 @@ processChatCommand = \case pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId - APIDeleteMyAddress userId -> withUserId userId $ \user -> withChatLock "deleteMyAddress" $ do + APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do conns <- withStore (`getUserAddressConnections` user) - procCmd $ do + withChatLock "deleteMyAddress" $ do deleteAgentConnectionsAsync user $ map aConnId conns withStore' (`deleteUserAddress` user) - pure $ CRUserContactLinkDeleted user + let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} + r <- updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing + let user' = case r of + CRUserProfileUpdated u' _ _ -> u' + _ -> user + pure $ CRUserContactLinkDeleted user' DeleteMyAddress -> withUser $ \User {userId} -> processChatCommand $ APIDeleteMyAddress userId APIShowMyAddress userId -> withUserId userId $ \user -> CRUserContactLink user <$> withStore (`getUserAddress` user) ShowMyAddress -> withUser $ \User {userId} -> processChatCommand $ APIShowMyAddress userId + APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do + let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} + updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing + APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do + ucl@UserContactLink {connReqContact} <- withStore (`getUserAddress` user) + let p' = (fromLocalProfile p :: Profile) {contactLink = Just connReqContact} + updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user $ Just ucl + SetProfileAddress onOff -> withUser $ \User {userId} -> + processChatCommand $ APISetProfileAddress userId onOff APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do contactLink <- withStore (\db -> updateUserAddressAutoAccept db user autoAccept_) pure $ CRUserContactLinkUpdated user contactLink @@ -1613,7 +1627,9 @@ processChatCommand = \case | chunks <= sendChunks && chunks * n <= totalSendChunks && isVoice mc = Just IFMSent | otherwise = Just IFMOffer updateProfile :: User -> Profile -> m ChatResponse - updateProfile user@User {profile = p} p' + updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p' + updateProfile_ :: User -> Profile -> m User -> m ChatResponse + updateProfile_ user@User {profile = p} p' updateUser | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user | otherwise = do -- read contacts before user update to correctly merge preferences @@ -1621,7 +1637,7 @@ processChatCommand = \case contacts <- filter (\ct -> isReady ct && not (contactConnIncognito ct)) <$> withStore' (`getUserContacts` user) - user' <- withStore $ \db -> updateUserProfile db user p' + user' <- updateUser asks currentUser >>= atomically . (`writeTVar` Just user') withChatLock "updateProfile" . procCmd $ do forM_ contacts $ \ct -> do @@ -4407,7 +4423,7 @@ getCreateActiveUser st = do loop = do displayName <- getContactName fullName <- T.pack <$> getWithPrompt "full name (optional)" - withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName, image = Nothing, preferences = Nothing} True) >>= \case + withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} True) >>= \case Left SEDuplicateName -> do putStrLn "chosen display name is already used by another profile on this device, choose another one" loop @@ -4713,6 +4729,8 @@ chatCommandP = ("/delete_address" <|> "/da") $> DeleteMyAddress, "/_show_address " *> (APIShowMyAddress <$> A.decimal), ("/show_address" <|> "/sa") $> ShowMyAddress, + "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), + ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), "/auto_accept " *> (AddressAutoAccept <$> autoAcceptP), ("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName), @@ -4766,7 +4784,7 @@ chatCommandP = pure (cName, fullName) userProfile = do (cName, fullName) <- userNames - pure Profile {displayName = cName, fullName, image = Nothing, preferences = Nothing} + pure Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bd0299d78f..54a780384e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -304,6 +304,8 @@ data ChatCommand | DeleteMyAddress | APIShowMyAddress UserId | ShowMyAddress + | APISetProfileAddress UserId Bool + | SetProfileAddress Bool | APIAddressAutoAccept UserId (Maybe AutoAccept) | AddressAutoAccept (Maybe AutoAccept) | AcceptContact ContactName diff --git a/src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs b/src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs new file mode 100644 index 0000000000..ee7ff053d5 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230422_profile_contact_links where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230422_profile_contact_links :: Query +m20230422_profile_contact_links = + [sql| +ALTER TABLE contact_profiles ADD COLUMN contact_link BLOB; +|] + +down_m20230422_profile_contact_links :: Query +down_m20230422_profile_contact_links = + [sql| +ALTER TABLE contact_profiles DROP COLUMN contact_link; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 5f24c8a3c3..c14ed3d211 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -17,7 +17,8 @@ CREATE TABLE contact_profiles( user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE, incognito INTEGER, local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL), - preferences TEXT + preferences TEXT, + contact_link BLOB ); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 59c6b8a10c..55a051ad80 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile generateRandomProfile = do adjective <- pick adjectives noun <- pickNoun adjective 2 - pure $ Profile {displayName = adjective <> noun, fullName = "", image = Nothing, preferences = Nothing} + pure $ Profile {displayName = adjective <> noun, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} where pick :: [a] -> IO a pick xs = (xs !!) <$> randomRIO (0, length xs - 1) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index f494c910ba..71de8efa4a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -55,6 +55,7 @@ module Simplex.Chat.Store getContact, getContactIdByName, updateUserProfile, + setUserProfileContactLink, updateContactProfile, updateContactUserPreferences, updateContactAlias, @@ -373,6 +374,7 @@ import Simplex.Chat.Migrations.M20230328_files_protocol import Simplex.Chat.Migrations.M20230402_protocol_servers import Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions import Simplex.Chat.Migrations.M20230420_rcv_files_to_receive +import Simplex.Chat.Migrations.M20230422_profile_contact_links import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) @@ -447,7 +449,8 @@ schemaMigrations = ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol), ("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers), ("20230411_extra_xftp_file_descriptions", m20230411_extra_xftp_file_descriptions, Just down_m20230411_extra_xftp_file_descriptions), - ("20230420_rcv_files_to_receive", m20230420_rcv_files_to_receive, Just down_m20230420_rcv_files_to_receive) + ("20230420_rcv_files_to_receive", m20230420_rcv_files_to_receive, Just down_m20230420_rcv_files_to_receive), + ("20230422_profile_contact_links", m20230422_profile_contact_links, Just down_m20230422_profile_contact_links) ] -- | The list of migrations in ascending order by date @@ -501,7 +504,7 @@ createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, pr (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, auId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences, True) :. (Nothing, Nothing) + pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences, True) :. (Nothing, Nothing) getUsersInfo :: DB.Connection -> IO [UserInfo] getUsersInfo db = getUsers db >>= mapM getUserInfo @@ -539,17 +542,17 @@ getUsers db = userQuery :: Query userQuery = [sql| - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.preferences, u.show_ntfs, u.view_pwd_hash, u.view_pwd_salt + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, u.show_ntfs, u.view_pwd_hash, u.view_pwd_salt FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences, Bool) :. (Maybe B64UrlByteString, Maybe B64UrlByteString) -> User -toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences, showNtfs) :. (viewPwdHash_, viewPwdSalt_)) = +toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences, Bool) :. (Maybe B64UrlByteString, Maybe B64UrlByteString) -> User +toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences, showNtfs) :. (viewPwdHash_, viewPwdSalt_)) = User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, viewPwdHash} where - profile = LocalProfile {profileId, displayName, fullName, image, preferences = userPreferences, localAlias = ""} + profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""} fullPreferences = mergePreferences Nothing userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ @@ -672,7 +675,7 @@ getConnReqContactXContactId db user@User {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, cp.local_alias, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, @@ -724,15 +727,15 @@ getProfileById db userId profileId = DB.query db [sql| - SELECT cp.display_name, cp.full_name, cp.image, cp.local_alias, cp.preferences -- , ct.user_preferences + SELECT cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, cp.preferences -- , ct.user_preferences FROM contact_profiles cp -- JOIN contacts ct ON cp.contact_profile_id = ct.contact_profile_id WHERE cp.user_id = ? AND cp.contact_profile_id = ? |] (userId, profileId) where - toProfile :: (ContactName, Text, Maybe ImageData, LocalAlias, Maybe Preferences) -> LocalProfile - toProfile (displayName, fullName, image, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} + toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile + toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, 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 @@ -765,12 +768,12 @@ createDirectContact db user@User {userId} activeConn@Connection {connId, localAl pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt} createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Maybe UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) -createContact_ db userId connId Profile {displayName, fullName, image, preferences} localAlias viaGroup currentTs chatTs = +createContact_ db userId connId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs chatTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, image, user_id, local_alias, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, image, userId, localAlias, preferences, currentTs, currentTs) + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, local_alias, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, localAlias, preferences, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -872,6 +875,23 @@ updateUserProfile db user p' profile = toLocalProfile profileId p' localAlias fullPreferences = mergePreferences Nothing preferences +setUserProfileContactLink :: DB.Connection -> User -> Maybe UserContactLink -> IO User +setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profileId}} ucl_ = do + ts <- getCurrentTime + DB.execute + db + [sql| + UPDATE contact_profiles + SET contact_link = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + |] + (connReqContact_, ts, userId, profileId) + pure (user :: User) {profile = p {contactLink = connReqContact_}} + where + connReqContact_ = case ucl_ of + Just UserContactLink {connReqContact} -> Just connReqContact + _ -> Nothing + updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact updateContactProfile db user@User {userId} c p' | displayName == newName = do @@ -964,15 +984,15 @@ updateContactProfile_ db userId profileId profile = do updateContactProfile_' db userId profileId profile currentTs updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () -updateContactProfile_' db userId profileId Profile {displayName, fullName, image, preferences} updatedAt = do +updateContactProfile_' db userId profileId Profile {displayName, fullName, image, contactLink, preferences} updatedAt = do DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ? + SET display_name = ?, full_name = ?, image = ?, contact_link = ?, preferences = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - (displayName, fullName, image, preferences, updatedAt, userId, profileId) + (displayName, fullName, image, contactLink, preferences, updatedAt, userId, profileId) updateContact_ :: DB.Connection -> UserId -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContact_ db userId contactId displayName newName updatedAt = do @@ -986,19 +1006,19 @@ 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, LocalAlias, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) toContact :: User -> ContactRow :. ConnectionRow -> Contact -toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} +toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} activeConn = toConnection connRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} +toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in case toMaybeConnection connRow of Just activeConn -> @@ -1023,14 +1043,14 @@ getUserContactProfiles db User {userId} = <$> DB.query db [sql| - SELECT display_name, full_name, image, preferences + SELECT display_name, full_name, image, contact_link, preferences FROM contact_profiles WHERE user_id = ? |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe Preferences) -> (Profile) - toContactProfile (displayName, fullName, image, preferences) = Profile {displayName, fullName, image, preferences} + toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> (Profile) + toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> ExceptT StoreError IO () createUserContactLink db User {userId} agentConnId cReq = @@ -1081,7 +1101,7 @@ getUserContactLinks db User {userId} = toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection connRow, UserContact {userContactLinkId, connReqContact, groupId}) deleteUserAddress :: DB.Connection -> User -> IO () -deleteUserAddress db User {userId} = do +deleteUserAddress db user@User {userId} = do DB.execute db [sql| @@ -1118,6 +1138,7 @@ deleteUserAddress db User {userId} = do ) |] [":user_id" := userId] + void $ setUserProfileContactLink db user Nothing DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL" (Only userId) data UserContactLink = UserContactLink @@ -1266,7 +1287,7 @@ setGroupLinkMemberRole db User {userId} userContactLinkId memberRole = DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId) createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest -createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ = +createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, contactLink, preferences} xContactId_ = liftIO (maybeM getContact' xContactId_) >>= \case Just contact -> pure $ CORContact contact Nothing -> CORRequest <$> createOrUpdate_ @@ -1288,8 +1309,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi createContactRequest_ currentTs ldn = do DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (displayName, fullName, image, userId, preferences, currentTs, currentTs) + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -1308,7 +1329,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi [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, cp.local_alias, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, @@ -1329,7 +1350,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at FROM contact_requests cr JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) @@ -1357,6 +1378,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi SET display_name = ?, full_name = ?, image = ?, + contact_link = ?, updated_at = ? WHERE contact_profile_id IN ( SELECT contact_profile_id @@ -1365,7 +1387,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi AND contact_request_id = ? ) |] - (displayName, fullName, image, currentTs, userId, cReqId) + (displayName, fullName, image, contactLink, currentTs, userId, cReqId) getContactRequest' :: DB.Connection -> Int64 -> ExceptT StoreError IO (User, UserContactRequest) getContactRequest' db contactRequestId = do @@ -1380,7 +1402,7 @@ getContactRequest db User {userId} contactRequestId = [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at FROM contact_requests cr JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) @@ -1389,11 +1411,11 @@ getContactRequest db User {userId} contactRequestId = |] (userId, contactRequestId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image) :. (xContactId, preferences, createdAt, updatedAt)) = do - let profile = Profile {displayName, fullName, image, preferences} +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt)) = do + let profile = Profile {displayName, fullName, image, contactLink, preferences} in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt} getContactRequestIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 @@ -1754,16 +1776,16 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do db [sql| SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, c.via_group, c.contact_used, c.enable_ntfs, + c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.enable_ntfs, p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts 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, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)] = - let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)] = + let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} @@ -1781,10 +1803,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.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, -- 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, p.local_alias, p.preferences + 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.contact_link, p.local_alias, p.preferences 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 @@ -1884,10 +1906,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.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, -- 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, p.local_alias, p.preferences, + 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.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m @@ -2120,7 +2142,7 @@ getUserGroupDetails db User {userId, userContactId} = [sql| SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at, g.chat_ts, 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, pu.local_alias, pu.preferences + 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.contact_link, pu.local_alias, pu.preferences FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -2167,7 +2189,7 @@ groupMemberQuery = [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, p.local_alias, p.preferences, + 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.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m @@ -2239,20 +2261,20 @@ getGroupInvitation db user groupId = firstRow fromOnly (SEGroupNotFound groupId) $ DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId) -type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, LocalAlias, Maybe Preferences)) +type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) -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, Maybe Preferences)) +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 ConnReqContact, Maybe LocalAlias, Maybe Preferences)) toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias, preferences)) = - let memberProfile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} +toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = + let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, 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 localAlias, contactPreferences)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias, contactPreferences)) +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, contactLink, Just localAlias, contactPreferences)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) toMaybeGroupMember _ _ = Nothing createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember @@ -2325,7 +2347,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} = [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, cp.local_alias, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, @@ -2380,13 +2402,13 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do -- | 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, preferences}) memCategory memStatus = +createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image, contactLink, preferences}) memCategory memStatus = ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do currentTs <- getCurrentTime DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (displayName, fullName, image, userId, preferences, currentTs, currentTs) + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) memProfileId <- insertedRowId db let newMember = NewGroupMember @@ -2629,10 +2651,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.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, -- 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, p.local_alias, p.preferences, + 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.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m @@ -2664,7 +2686,7 @@ getViaGroupContact db user@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, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter @@ -2681,9 +2703,9 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)) :. ConnectionRow -> Contact - toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} + toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)) :. ConnectionRow -> Contact + toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} activeConn = toConnection connRow mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn @@ -3698,7 +3720,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.local_alias, p.preferences + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences 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 @@ -3738,7 +3760,7 @@ getDirectChatPreviews_ db user@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, cp.local_alias, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, @@ -3807,7 +3829,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.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat, -- ChatItem @@ -3817,17 +3839,17 @@ 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.local_alias, p.preferences, + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, -- 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.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category, dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.image, dbp.local_alias, dbp.preferences + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences 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 @@ -3873,7 +3895,7 @@ getContactRequestChatPreviews_ db User {userId} = [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at FROM contact_requests cr JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id @@ -4068,7 +4090,7 @@ getContact db user@User {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, cp.local_alias, ct.contact_used, ct.enable_ntfs, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, @@ -4197,7 +4219,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.local_alias, pu.preferences + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences 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 @@ -4595,17 +4617,17 @@ 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.local_alias, p.preferences, + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, -- 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.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category, dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.image, dbp.local_alias, dbp.preferences + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences 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 22b0041281..de89599f2d 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1071,6 +1071,7 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, + contactLink :: Maybe ConnReqContact, preferences :: Maybe Preferences -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- - contact_profile_id @@ -1099,6 +1100,7 @@ data LocalProfile = LocalProfile displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, + contactLink :: Maybe ConnReqContact, preferences :: Maybe Preferences, localAlias :: LocalAlias } @@ -1112,12 +1114,12 @@ localProfileId :: LocalProfile -> ProfileId localProfileId = profileId toLocalProfile :: ProfileId -> Profile -> LocalAlias -> LocalProfile -toLocalProfile profileId Profile {displayName, fullName, image, preferences} localAlias = - LocalProfile {profileId, displayName, fullName, image, preferences, localAlias} +toLocalProfile profileId Profile {displayName, fullName, image, contactLink, preferences} localAlias = + LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} fromLocalProfile :: LocalProfile -> Profile -fromLocalProfile LocalProfile {displayName, fullName, image, preferences} = - Profile {displayName, fullName, image, preferences} +fromLocalProfile LocalProfile {displayName, fullName, image, contactLink, preferences} = + Profile {displayName, fullName, image, contactLink, preferences} data GroupProfile = GroupProfile { displayName :: GroupName, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 5eedb8e638..f4ae01c863 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -811,8 +811,9 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = ] viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewConnectionStats stats + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) @@ -872,11 +873,12 @@ viewSwitchPhase SPCompleted = "changed address" viewSwitchPhase phase = plain (strEncode phase) <> " changing address" viewUserProfileUpdated :: Profile -> Profile -> [StyledString] -viewUserProfileUpdated Profile {displayName = n, fullName, image, preferences} Profile {displayName = n', fullName = fullName', image = image', preferences = prefs'} = +viewUserProfileUpdated Profile {displayName = n, fullName, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', image = image', contactLink = contactLink', preferences = prefs'} = profileUpdated <> viewPrefsUpdated preferences prefs' where profileUpdated - | n == n' && fullName == fullName' && image == image' = [] + | n == n' && fullName == fullName' && image == image' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] | n == n' && fullName == fullName' = [if isNothing image' then "profile image removed" else "profile image updated"] | n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified] | otherwise = ["user profile is changed to " <> ttyFullName n' fullName' <> notified] @@ -980,9 +982,13 @@ viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias} viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated - Contact {localDisplayName = n, profile = LocalProfile {fullName}} - Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName'}} - | n == n' && fullName == fullName' = [] + Contact {localDisplayName = n, profile = LocalProfile {fullName, contactLink}} + Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', contactLink = contactLink'}} + | n == n' && fullName == fullName' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' = + if isNothing contactLink' + then [ttyContact n <> " removed contact address"] + else [ttyContact n <> " set new contact address, use " <> highlight ("/info " <> n) <> " to view"] | n == n' = ["contact " <> ttyContact n <> fullNameUpdate] | otherwise = [ "contact " <> ttyContact n <> " changed to " <> ttyFullName n' fullName', diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index cba818295f..a5dc083f71 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -19,6 +19,7 @@ chatProfileTests = do it "update user profile with image" testUpdateProfileImage describe "user contact link" $ do describe "create and connect via contact link" testUserContactLink + it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange @@ -134,6 +135,83 @@ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath +testProfileLink :: HasCallStack => FilePath -> IO () +testProfileLink = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + alice ##> "/ad" + cLink <- getContactLink alice True + + bob ##> ("/c " <> cLink) + alice <#? bob + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + + alice ##> "/pa on" + alice <## "new contact address set" + + bob <## "alice set new contact address, use /info alice to view" + checkAliceProfileLink bob cLink + + cath ##> ("/c " <> cLink) + alice <#? cath + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request..." + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + alice <##> cath + + checkAliceProfileLink cath cLink + + alice ##> "/pa off" + alice <## "contact address removed" + + bob <## "alice removed contact address" + checkAliceNoProfileLink bob + + cath <## "alice removed contact address" + checkAliceNoProfileLink cath + + alice ##> "/pa on" + alice <## "new contact address set" + + bob <## "alice set new contact address, use /info alice to view" + checkAliceProfileLink bob cLink + + cath <## "alice set new contact address, use /info alice to view" + checkAliceProfileLink cath cLink + + alice ##> "/da" + alice <## "Your chat address is deleted - accepted contacts will remain connected." + alice <## "To create a new chat address use /ad" + + bob <## "alice removed contact address" + checkAliceNoProfileLink bob + + cath <## "alice removed contact address" + checkAliceNoProfileLink cath + where + checkAliceProfileLink cc cLink = do + cc ##> "/info alice" + cc <## "contact ID: 2" + cc <##. "receiving messages via" + cc <##. "sending messages via" + cc <## ("contact address: " <> cLink) + cc <## "you've shared main profile with this contact" + cc <## "connection not verified, use /code command to see security code" + checkAliceNoProfileLink cc = do + cc ##> "/info alice" + cc <## "contact ID: 2" + cc <##. "receiving messages via" + cc <##. "sending messages via" + cc <## "you've shared main profile with this contact" + cc <## "connection not verified, use /code command to see security code" + testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO () testUserContactLinkAutoAccept = testChat4 aliceProfile bobProfile cathProfile danProfile $ diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index a373b7cea6..18866fe2d1 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -31,16 +31,16 @@ defaultPrefs :: Maybe Preferences defaultPrefs = Just $ toChatPrefs defaultChatPrefs aliceProfile :: Profile -aliceProfile = Profile {displayName = "alice", fullName = "Alice", image = Nothing, preferences = defaultPrefs} +aliceProfile = Profile {displayName = "alice", fullName = "Alice", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} bobProfile :: Profile -bobProfile = Profile {displayName = "bob", fullName = "Bob", image = Just (ImageData "data:image/png;base64,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"), preferences = defaultPrefs} +bobProfile = Profile {displayName = "bob", fullName = "Bob", image = Just (ImageData "data:image/png;base64,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"), contactLink = Nothing, preferences = defaultPrefs} cathProfile :: Profile -cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Nothing, preferences = defaultPrefs} +cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} danProfile :: Profile -danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, preferences = defaultPrefs} +danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} xit' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) xit' = if os == "linux" then xit else it diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 4c1d056bf7..dcc7e88c9f 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -92,7 +92,7 @@ testGroupPreferences :: Maybe GroupPreferences testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, voice = Just VoiceGroupPreference {enable = FEOn}, fullDelete = Nothing} testProfile :: Profile -testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), preferences = testChatPreferences} +testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences} @@ -198,7 +198,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XInfo testProfile it "x.info with empty full name" $ "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}" - #==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, preferences = testChatPreferences} + #==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences} it "x.contact with xContactId" $ "{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") From 69aa002c83f059fb288a0ab3479339bdc8ba1c1b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 27 Apr 2023 18:44:12 +0300 Subject: [PATCH 05/22] android: prevent click & long click at the same time (#2237) --- .../simplex/app/views/chat/item/TextItemView.kt | 2 +- .../simplex/app/views/helpers/GestureDetector.kt | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index dafc328cf5..d6fe1b2f4c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -228,7 +228,7 @@ fun ClickableText( } } }, shouldConsumeEvent = { pos -> - var consume = false + var consume = false layoutResult.value?.let { layoutResult -> consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos)) } 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 41d6c73190..463d0eacbd 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 @@ -80,10 +80,16 @@ suspend fun PointerInputScope.detectGesture( pressScope.release() } } catch (_: PointerEventTimeoutCancellationException) { - onLongPress?.invoke(down.position) - if (shouldConsume) - consumeUntilUp() - pressScope.release() + if (onLongPress != null) { + onLongPress(down.position) + if (shouldConsume) + consumeUntilUp() + pressScope.cancel() + } else { + if (shouldConsume) + consumeUntilUp() + pressScope.release() + } } } } From 59f3848056e19bdb3710beaf27b45604172edafc Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 27 Apr 2023 19:00:03 +0300 Subject: [PATCH 06/22] android: cancel and destructive buttons in alerts (#2238) --- .../main/java/chat/simplex/app/MainActivity.kt | 2 +- .../chat/simplex/app/views/chat/ChatInfoView.kt | 15 +++++++++------ .../app/views/chat/group/GroupChatInfoView.kt | 10 ++++++---- .../simplex/app/views/chat/group/GroupLinkView.kt | 5 +++-- .../app/views/chat/group/GroupMemberInfoView.kt | 5 +++-- .../simplex/app/views/chat/item/ChatItemView.kt | 4 ++-- .../app/views/chatlist/ChatListNavLinkView.kt | 4 +++- .../simplex/app/views/database/ChatArchiveView.kt | 3 ++- .../app/views/database/DatabaseEncryptionView.kt | 2 +- .../simplex/app/views/database/DatabaseView.kt | 9 ++++++--- .../simplex/app/views/helpers/AlertManager.kt | 7 ++----- .../simplex/app/views/newchat/PasteToConnect.kt | 2 +- .../app/views/newchat/ScanToConnectView.kt | 2 +- .../views/usersettings/AdvancedNetworkSettings.kt | 2 +- .../app/views/usersettings/NetworkAndServers.kt | 4 ++-- .../simplex/app/views/usersettings/RTCServers.kt | 3 ++- .../app/views/usersettings/UserAddressView.kt | 5 +++-- 17 files changed, 48 insertions(+), 36 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 953b598630..49d3f75c16 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 @@ -633,7 +633,7 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link) ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link) } - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = title, text = if (linkType == ConnectionLinkType.GROUP) generalGetString(R.string.you_will_join_group) 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 c74b579f08..12b21f9ddc 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 @@ -111,7 +111,7 @@ fun ChatInfoView( } fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.delete_contact_question), text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning), confirmText = generalGetString(R.string.delete_verb), @@ -125,12 +125,13 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> close?.invoke() } } - } + }, + destructive = true, ) } fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.clear_chat_question), text = generalGetString(R.string.clear_chat_warning), confirmText = generalGetString(R.string.clear_verb), @@ -143,7 +144,8 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit close?.invoke() } } - } + }, + destructive = true, ) } @@ -421,13 +423,14 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C } private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.switch_receiving_address_question), text = generalGetString(R.string.switch_receiving_address_desc), confirmText = generalGetString(R.string.switch_verb), onConfirm = { switchContactAddress(m, contactId) - } + }, + destructive = true, ) } 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 89c9d1b1dc..7eb93e2d32 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 @@ -108,7 +108,7 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM val alertTextKey = if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning else R.string.delete_group_for_self_cannot_undo_warning - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.delete_group_question), text = generalGetString(alertTextKey), confirmText = generalGetString(R.string.delete_verb), @@ -122,12 +122,13 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM close?.invoke() } } - } + }, + destructive = true, ) } fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.leave_group_question), text = generalGetString(R.string.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), confirmText = generalGetString(R.string.leave_group_button), @@ -136,7 +137,8 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U chatModel.controller.leaveGroup(groupInfo.groupId) close?.invoke() } - } + }, + destructive = true, ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt index a044e9cc1b..6fb2eaf9d3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt @@ -66,7 +66,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St } }, deleteLink = { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.delete_link_question), text = generalGetString(R.string.all_group_members_will_remain_connected), confirmText = generalGetString(R.string.delete_verb), @@ -78,7 +78,8 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St onGroupLinkUpdated(null to null) } } - } + }, + destructive = true, ) } ) 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 0ab3e38cf9..612b017823 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 @@ -116,7 +116,7 @@ fun GroupMemberInfoView( } fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( 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), @@ -128,7 +128,8 @@ fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: Cha } close?.invoke() } - } + }, + destructive = true, ) } 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 5e64d13e8d..96c6fbca76 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 @@ -380,13 +380,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) AlertManager.shared.hideAlert() - }) { Text(stringResource(R.string.for_me_only)) } + }) { Text(stringResource(R.string.for_me_only), color = MaterialTheme.colors.error) } if (chatItem.meta.editable) { Spacer(Modifier.padding(horizontal = 4.dp)) TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) AlertManager.shared.hideAlert() - }) { Text(stringResource(R.string.for_everybody)) } + }) { Text(stringResource(R.string.for_everybody), color = MaterialTheme.colors.error) } } } } 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 921197a6f7..fa8b639b96 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 @@ -466,7 +466,8 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel onSuccess() } } - } + }, + destructive = true, ) } @@ -484,6 +485,7 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) { } } }, + destructive = true, dismissText = generalGetString(R.string.cancel_verb), ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt index 5dc5b55faf..6df30f9170 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt @@ -116,7 +116,8 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { } else { Log.e(TAG, "deleteArchiveAlert delete() error") } - } + }, + destructive = true, ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt index a2e5993e07..5c24e59bd4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt @@ -244,7 +244,7 @@ fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), confirmText = generalGetString(R.string.encrypt_database), onConfirm = onConfirm, - destructive = false, + destructive = true, ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index 8ac6f8cb40..5feaa4bdff 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -294,7 +294,8 @@ private fun setChatItemTTLAlert( text = generalGetString(R.string.enable_automatic_deletion_message), confirmText = generalGetString(R.string.delete_messages), onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize, context) }, - onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value } + onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value }, + destructive = true, ) } @@ -543,7 +544,8 @@ private fun importArchiveAlert( title = generalGetString(R.string.import_database_question), text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), confirmText = generalGetString(R.string.import_database_confirmation), - onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) } + onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) }, + destructive = true, ) } @@ -608,7 +610,8 @@ private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState Unit)? = null + confirmText: String = generalGetString(R.string.ok) ) { showAlert { AlertDialog( @@ -163,7 +163,6 @@ class AlertManager { horizontalArrangement = Arrangement.Center ) { TextButton(onClick = { - onConfirm?.invoke() hideAlert() }) { Text(confirmText, color = Color.Unspecified) } } @@ -173,13 +172,11 @@ class AlertManager { ) } } - fun showAlertMsg( title: Int, text: Int? = null, confirmText: Int = R.string.ok, - onConfirm: (() -> Unit)? = null - ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm) + ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText)) @Composable fun showInView() { 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 c69f9c18fb..19a35a0f6d 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 @@ -45,7 +45,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { } } if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.connect_via_group_link), text = generalGetString(R.string.you_will_join_group), confirmText = generalGetString(R.string.connect_via_link_verb), 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 4f6e4c41d0..d1210c8e16 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 @@ -48,7 +48,7 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { } } if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.connect_via_group_link), text = generalGetString(R.string.you_will_join_group), confirmText = generalGetString(R.string.connect_via_link_verb), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt index a8841e824c..fe1f29802d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt @@ -387,7 +387,7 @@ fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boo } fun showUpdateNetworkSettingsDialog(action: () -> Unit) { - AlertManager.shared.showAlertMsg( + 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), 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 fce9aa3894..dcaad615cf 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 @@ -59,7 +59,7 @@ fun NetworkAndServersView( showCustomModal = showCustomModal, toggleSocksProxy = { enable -> if (enable) { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.network_enable_socks), text = generalGetString(R.string.network_enable_socks_info), confirmText = generalGetString(R.string.confirm_verb), @@ -73,7 +73,7 @@ fun NetworkAndServersView( } ) } else { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.network_disable_socks), text = generalGetString(R.string.network_disable_socks_info), confirmText = generalGetString(R.string.confirm_verb), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt index c37d2a95bb..fde4082739 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt @@ -70,7 +70,8 @@ fun RTCServersView( resetRTCServers() isUserRTCServers = false userRTCServersStr.value = "" - } + }, + destructive = true, ) } else { isUserRTCServers = false diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt index 574cc158a3..6a2cad337e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt @@ -44,7 +44,7 @@ fun UserAddressView(chatModel: ChatModel) { } }, deleteAddress = { - AlertManager.shared.showAlertMsg( + AlertManager.shared.showAlertDialog( title = generalGetString(R.string.delete_address__question), text = generalGetString(R.string.all_your_contacts_will_remain_connected), confirmText = generalGetString(R.string.delete_verb), @@ -53,7 +53,8 @@ fun UserAddressView(chatModel: ChatModel) { chatModel.controller.apiDeleteUserAddress() chatModel.userAddress.value = null } - } + }, + destructive = true, ) } ) From c254b337531339dd309fcc13538dfb7454a9ddd6 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 27 Apr 2023 22:32:40 +0300 Subject: [PATCH 07/22] android: blue theme and more options (#2331) * android: blue theme and more options * more color options and better code * more color options and some fixes * removed preferences about non-existent theme * colors * Revert "removed preferences about non-existent theme" This reverts commit cbb38d54a81046211ade7f2ac40e65d95d658007. * colors * update colors * migrations * HighOrLowLight -> secondary * new color * color * update colors, move colors to a separate page --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/MainActivity.kt | 28 +-- .../main/java/chat/simplex/app/SimplexApp.kt | 19 ++ .../java/chat/simplex/app/model/ChatModel.kt | 2 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 34 ++- .../java/chat/simplex/app/ui/theme/Color.kt | 1 - .../java/chat/simplex/app/ui/theme/Theme.kt | 206 +++++++++++++++++- .../chat/simplex/app/ui/theme/ThemeManager.kt | 119 ++++++++-- .../java/chat/simplex/app/views/SplashView.kt | 5 +- .../chat/simplex/app/views/TerminalView.kt | 2 +- .../chat/simplex/app/views/WelcomeView.kt | 8 +- .../chat/simplex/app/views/call/CallView.kt | 4 +- .../app/views/call/IncomingCallActivity.kt | 6 +- .../app/views/call/IncomingCallAlertView.kt | 4 +- .../simplex/app/views/chat/ChatInfoView.kt | 16 +- .../chat/simplex/app/views/chat/ChatView.kt | 6 +- .../simplex/app/views/chat/ComposeFileView.kt | 5 +- .../app/views/chat/ComposeImageView.kt | 9 +- .../simplex/app/views/chat/ComposeView.kt | 3 +- .../app/views/chat/ComposeVoiceView.kt | 8 +- .../app/views/chat/ContactPreferences.kt | 4 +- .../simplex/app/views/chat/ContextItemView.kt | 10 +- .../simplex/app/views/chat/SendMsgView.kt | 18 +- .../simplex/app/views/chat/VerifyCodeView.kt | 2 +- .../views/chat/group/AddGroupMembersView.kt | 16 +- .../app/views/chat/group/GroupChatInfoView.kt | 16 +- .../app/views/chat/group/GroupLinkView.kt | 5 +- .../views/chat/group/GroupMemberInfoView.kt | 2 +- .../app/views/chat/group/GroupPreferences.kt | 6 +- .../app/views/chat/group/GroupProfileView.kt | 4 +- .../views/chat/group/WelcomeMessageView.kt | 2 +- .../app/views/chat/item/CICallItemView.kt | 11 +- .../app/views/chat/item/CIEventView.kt | 36 ++- .../chat/item/CIFeaturePreferenceView.kt | 5 +- .../simplex/app/views/chat/item/CIFileView.kt | 2 +- .../views/chat/item/CIGroupInvitationView.kt | 7 +- .../simplex/app/views/chat/item/CIMetaView.kt | 6 +- .../app/views/chat/item/CIVoiceView.kt | 8 +- .../app/views/chat/item/ChatItemView.kt | 2 +- .../app/views/chat/item/DeletedItemView.kt | 10 +- .../app/views/chat/item/FramedItemView.kt | 26 ++- .../views/chat/item/IntegrityErrorItemView.kt | 5 +- .../views/chat/item/MarkedDeletedItemView.kt | 10 +- .../app/views/chat/item/TextItemView.kt | 4 +- .../app/views/chatlist/ChatListNavLinkView.kt | 2 +- .../app/views/chatlist/ChatListView.kt | 11 +- .../app/views/chatlist/ChatPreviewView.kt | 26 +-- .../views/chatlist/ContactConnectionView.kt | 4 +- .../app/views/chatlist/ContactRequestView.kt | 2 +- .../app/views/chatlist/ShareListView.kt | 7 +- .../simplex/app/views/chatlist/UserPicker.kt | 8 +- .../views/database/DatabaseEncryptionView.kt | 10 +- .../app/views/database/DatabaseErrorView.kt | 2 +- .../app/views/database/DatabaseView.kt | 8 +- .../simplex/app/views/helpers/AlertManager.kt | 10 +- .../app/views/helpers/ChatInfoImage.kt | 6 +- .../app/views/helpers/CloseSheetBar.kt | 15 +- .../views/helpers/DefaultBasicTextField.kt | 5 +- .../app/views/helpers/DefaultDropdownMenu.kt | 9 +- .../app/views/helpers/DefaultSwitch.kt | 5 +- .../app/views/helpers/DefaultTopAppBar.kt | 2 +- .../helpers/ExposedDropDownSettingRow.kt | 6 +- .../simplex/app/views/helpers/LinkPreviews.kt | 12 +- .../simplex/app/views/helpers/ModalView.kt | 3 +- .../chat/simplex/app/views/helpers/Section.kt | 18 +- .../simplex/app/views/helpers/SimpleButton.kt | 11 +- .../simplex/app/views/helpers/TextEditor.kt | 5 +- .../chat/simplex/app/views/helpers/Util.kt | 21 ++ .../app/views/localauth/PasswordEntry.kt | 5 +- .../app/views/newchat/AddContactView.kt | 4 +- .../simplex/app/views/newchat/AddGroupView.kt | 2 +- .../app/views/newchat/ConnectViaLinkView.kt | 6 +- .../app/views/newchat/CreateLinkView.kt | 6 +- .../simplex/app/views/newchat/NewChatSheet.kt | 6 +- .../views/onboarding/SetNotificationsMode.kt | 4 +- .../app/views/onboarding/WhatsNewView.kt | 21 +- .../views/usersettings/AcceptRequestsView.kt | 4 +- .../usersettings/AdvancedNetworkSettings.kt | 20 +- .../app/views/usersettings/Appearance.kt | 197 ++++++++++++++--- .../app/views/usersettings/CallSettings.kt | 3 +- .../views/usersettings/HiddenProfileView.kt | 2 +- .../views/usersettings/NetworkAndServers.kt | 2 +- .../usersettings/NotificationsSettingsView.kt | 4 +- .../app/views/usersettings/Preferences.kt | 6 +- .../views/usersettings/ProtocolServerView.kt | 8 +- .../views/usersettings/ProtocolServersView.kt | 18 +- .../app/views/usersettings/RTCServers.kt | 2 +- .../app/views/usersettings/SettingsView.kt | 49 +++-- .../app/views/usersettings/UserProfileView.kt | 4 +- .../views/usersettings/UserProfilesView.kt | 2 +- .../app/src/main/res/values/strings.xml | 16 ++ 91 files changed, 896 insertions(+), 406 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 0a256bbb99..2291fcd1dd 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -119,6 +119,7 @@ dependencies { implementation 'androidx.fragment:fragment:1.4.1' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation 'com.charleskorn.kaml:kaml:0.43.0' //implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation "androidx.compose.ui:ui-util:$compose_version" implementation "androidx.navigation:navigation-compose:2.4.1" 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 49d3f75c16..029fa52b65 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 @@ -17,6 +17,7 @@ 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 import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -26,8 +27,7 @@ import androidx.lifecycle.* import chat.simplex.app.MainActivity.Companion.enteredBackground import chat.simplex.app.model.* import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent -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.SplashView import chat.simplex.app.views.call.ActiveCallView import chat.simplex.app.views.call.IncomingCallAlertView @@ -83,20 +83,14 @@ class MainActivity: FragmentActivity() { } setContent { SimpleXTheme { - Surface( - Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize() - ) { - MainPage( - m, - userAuthorized, - laFailed, - ::runAuthenticate, - ::setPerformLA, - showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) } - ) - } + MainPage( + m, + userAuthorized, + laFailed, + ::runAuthenticate, + ::setPerformLA, + showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) } + ) } } SimplexApp.context.schedulePeriodicServiceRestartWorker() @@ -420,7 +414,7 @@ fun MainPage( @Composable fun authView() { Box( - Modifier.fillMaxSize(), + Modifier.fillMaxSize().background(MaterialTheme.colors.background), contentAlignment = Alignment.Center ) { SimpleButton( 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 1559002080..2a6d7587bc 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 @@ -6,6 +6,7 @@ import android.util.Log import androidx.lifecycle.* import androidx.work.* import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.DefaultTheme import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.usersettings.NotificationsMode @@ -95,6 +96,7 @@ class SimplexApp: Application(), LifecycleEventObserver { initChatController() ProcessLifecycleOwner.get().lifecycle.addObserver(this) context.getDir("temp", MODE_PRIVATE).deleteRecursively() + runMigrations() } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { @@ -184,6 +186,23 @@ class SimplexApp: Application(), LifecycleEventObserver { MessagesFetcherWorker.scheduleWork() } + private fun runMigrations() { + val lastMigration = chatModel.controller.appPrefs.lastMigratedVersionCode + if (lastMigration.get() < BuildConfig.VERSION_CODE) { + while (true) { + if (lastMigration.get() < 117) { + if (chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) { + chatModel.controller.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) + } + lastMigration.set(117) + } else { + lastMigration.set(BuildConfig.VERSION_CODE) + break + } + } + } + } + companion object { lateinit var context: SimplexApp private set 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 0f54b42d8c..8947c782fd 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 @@ -1516,7 +1516,7 @@ data class CIMeta ( val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew - fun statusIcon(primaryColor: Color, metaColor: Color = HighOrLowlight): Pair? = + fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair? = when (itemStatus) { is CIStatus.SndSent -> R.drawable.ic_check_filled to metaColor is CIStatus.SndErrorAuth -> R.drawable.ic_close to Color.Red 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 1fe7e1aa1a..662867f368 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 @@ -14,7 +14,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -26,10 +25,14 @@ import chat.simplex.app.views.call.* import chat.simplex.app.views.newchat.ConnectViaLinkTab import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.usersettings.* +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.YamlConfiguration import kotlinx.coroutines.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.* import java.util.Date @@ -57,6 +60,7 @@ enum class SimplexLinkMode { class AppPreferences(val context: Context) { private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) + private val sharedPreferencesThemes: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE) // deprecated, remove in 2024 private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) @@ -147,9 +151,15 @@ class AppPreferences(val context: Context) { val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) - val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb()) + val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) + val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = { + json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) + }, decode = { + json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) + }, sharedPreferencesThemes) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) + val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( @@ -204,8 +214,15 @@ class AppPreferences(val context: Context) { } ) + private fun mkMapPreference(prefName: String, default: Map, encode: (Map) -> String, decode: (String) -> Map, prefs: SharedPreferences = sharedPreferences): SharedPreference> = + SharedPreference( + get = fun() = decode(prefs.getString(prefName, encode(default))!!), + set = fun(value) = prefs.edit().putString(prefName, encode(value)).apply() + ) + companion object { internal const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" + internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground" private const val SHARED_PREFS_NOTIFICATIONS_MODE = "NotificationsMode" @@ -258,8 +275,10 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" - private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor" + private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" + private const val SHARED_PREFS_THEMES = "Themes" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" + private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" } } @@ -2581,7 +2600,7 @@ data class FeatureEnabled( } val iconColor: Color - get() = if (forUser) SimplexGreen else if (forContact) WarningYellow else HighOrLowlight + get() = if (forUser) SimplexGreen else if (forContact) WarningYellow else CurrentColors.value.colors.secondary companion object { fun enabled(asymmetric: Boolean, user: ChatPreference, contact: ChatPreference): FeatureEnabled = @@ -2965,7 +2984,7 @@ enum class GroupFeatureEnabled { } val iconColor: Color - get() = if (this == ON) SimplexGreen else HighOrLowlight + get() = if (this == ON) SimplexGreen else CurrentColors.value.colors.secondary } @@ -2976,6 +2995,11 @@ val json = Json { explicitNulls = false } +val yaml = Yaml(configuration = YamlConfiguration( + strictMode = false, + encodeDefaults = false, +)) + @Serializable class APIResponse(val resp: CR, val corr: String? = null) { companion object { 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 e0187c60e4..ee2997b428 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 @@ -13,7 +13,6 @@ val SimplexGreen = Color(77, 218, 103, 255) val SecretColor = Color(0x40808080) val LightGray = Color(241, 242, 246, 255) val DarkGray = Color(43, 44, 46, 255) -val DarkGrayBackground = Color(38, 38, 38, 255) val HighOrLowlight = Color(139, 135, 134, 255) val MessagePreviewDark = Color(179, 175, 174, 255) val MessagePreviewLight = Color(49, 45, 44, 255) 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 ccd7cce6f2..1f1b4dcdeb 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,16 +2,165 @@ package chat.simplex.app.ui.theme import android.app.UiModeManager import android.content.Context +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.* import androidx.compose.ui.unit.dp +import chat.simplex.app.R import chat.simplex.app.SimplexApp +import chat.simplex.app.views.helpers.* import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable enum class DefaultTheme { - SYSTEM, DARK, LIGHT + SYSTEM, LIGHT, DARK, SIMPLEX; + + // Call it only with base theme, not SYSTEM + fun hasChangedAnyColor(colors: Colors): Boolean { + val palette = when (this) { + SYSTEM -> return false + LIGHT -> LightColorPalette + DARK -> DarkColorPalette + SIMPLEX -> SimplexColorPalette + } + return with(palette) { + colors.primary != primary || + colors.primaryVariant != primaryVariant || + colors.secondary != secondary || + colors.secondaryVariant != secondaryVariant || + colors.background != background || + colors.surface != surface + } + } +} + +data class AppColors( + val title: Color, + val sentMessage: Color, + val receivedMessage: Color +) + +enum class ThemeColor { + PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE; + + fun fromColors(colors: Colors, appColors: AppColors): Color { + return when (this) { + PRIMARY -> colors.primary + PRIMARY_VARIANT -> colors.primaryVariant + SECONDARY -> colors.secondary + SECONDARY_VARIANT -> colors.secondaryVariant + BACKGROUND -> colors.background + SURFACE -> colors.surface + TITLE -> appColors.title + SENT_MESSAGE -> appColors.sentMessage + RECEIVED_MESSAGE -> appColors.receivedMessage + } + } + + val text: String + get() = when (this) { + PRIMARY -> generalGetString(R.string.color_primary) + PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant) + SECONDARY -> generalGetString(R.string.color_secondary) + SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant) + BACKGROUND -> generalGetString(R.string.color_background) + SURFACE -> generalGetString(R.string.color_surface) + TITLE -> generalGetString(R.string.color_title) + SENT_MESSAGE -> generalGetString(R.string.color_sent_message) + RECEIVED_MESSAGE -> generalGetString(R.string.color_received_message) + } +} + +@Serializable +data class ThemeColors( + @SerialName("accent") + val primary: String? = null, + @SerialName("accentVariant") + val primaryVariant: String? = null, + val secondary: String? = null, + val secondaryVariant: String? = null, + val background: String? = null, + @SerialName("menus") + val surface: String? = null, + val title: String? = null, + val sentMessage: String? = null, + val receivedMessage: String? = null, +) { + fun toColors(base: DefaultTheme): Colors { + val baseColors = when (base) { + DefaultTheme.LIGHT -> LightColorPalette + DefaultTheme.DARK -> DarkColorPalette + DefaultTheme.SIMPLEX -> SimplexColorPalette + // shouldn't be here + DefaultTheme.SYSTEM -> LightColorPalette + } + return baseColors.copy( + primary = primary?.colorFromReadableHex() ?: baseColors.primary, + primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, + secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary, + secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, + background = background?.colorFromReadableHex() ?: baseColors.background, + surface = surface?.colorFromReadableHex() ?: baseColors.surface, + ) + } + + fun toAppColors(base: DefaultTheme): AppColors { + val baseColors = when (base) { + DefaultTheme.LIGHT -> LightColorPaletteApp + DefaultTheme.DARK -> DarkColorPaletteApp + DefaultTheme.SIMPLEX -> SimplexColorPaletteApp + // shouldn't be here + DefaultTheme.SYSTEM -> LightColorPaletteApp + } + return baseColors.copy( + title = title?.colorFromReadableHex() ?: baseColors.title, + sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, + receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, + ) + } +} + +private fun String.colorFromReadableHex(): Color = + Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) + +@Serializable +data class ThemeOverrides ( + val base: DefaultTheme, + val colors: ThemeColors +) { + fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides { + return copy(colors = when (name) { + ThemeColor.PRIMARY -> colors.copy(primary = color) + ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) + ThemeColor.SECONDARY -> colors.copy(secondary = color) + ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color) + ThemeColor.BACKGROUND -> colors.copy(background = color) + ThemeColor.SURFACE -> colors.copy(surface = color) + ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) + }) + } +} + +fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { + return if (baseTheme == DefaultTheme.SIMPLEX) { + this.background(brush = Brush.linearGradient( + listOf( + CurrentColors.value.colors.background.darker(0.4f), + CurrentColors.value.colors.background.lighter(0.4f) + ), + Offset(0f, Float.POSITIVE_INFINITY), + Offset(Float.POSITIVE_INFINITY, 0f) + ), shape = shape) + } else { + this.background(color = CurrentColors.value.colors.background, shape = shape) + } } val DEFAULT_PADDING = 20.dp @@ -22,10 +171,11 @@ val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp val DarkColorPalette = darkColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files - primaryVariant = SimplexGreen, - secondary = DarkGray, + primaryVariant = SimplexBlue, + secondary = HighOrLowlight, + secondaryVariant = DarkGray, // background = Color.Black, -// surface = Color.Black, + surface = Color(0xFF222222), // background = Color(0xFF121212), // surface = Color(0xFF121212), error = Color.Red, @@ -33,27 +183,59 @@ val DarkColorPalette = darkColors( onSurface = Color(0xFFFFFBFA), // onError: Color = Color.Black, ) +val DarkColorPaletteApp = AppColors( + title = SimplexBlue, + sentMessage = Color(0x1E45B8FF), + receivedMessage = Color(0x20B1B0B5) +) + val LightColorPalette = lightColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files - primaryVariant = SimplexGreen, - secondary = LightGray, + primaryVariant = SimplexBlue, + secondary = HighOrLowlight, + secondaryVariant = LightGray, error = Color.Red, // background = Color.White, -// surface = Color.White + surface = Color.White, // onPrimary = Color.White, // onSecondary = Color.Black, // onBackground = Color.Black, // onSurface = Color.Black, ) +val LightColorPaletteApp = AppColors( + title = SimplexBlue, + sentMessage = Color(0x1E45B8FF), + receivedMessage = Color(0x20B1B0B5) +) -val CurrentColors: MutableStateFlow> = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) +val SimplexColorPalette = darkColors( + primary = Color(0xFF70F0F9), // If this value changes also need to update #0088ff in string resource files + primaryVariant = Color(0xFF1298A5), + secondary = HighOrLowlight, + secondaryVariant = Color(0xFF2C464D), + background = Color(0xFF111528), + // surface = Color.Black, + // background = Color(0xFF121212), + surface = Color(0xFF121C37), + error = Color.Red, +// onBackground = Color(0xFFFFFBFA), +// onSurface = Color(0xFFFFFBFA), + // onError: Color = Color.Black, +) +val SimplexColorPaletteApp = AppColors( + title = Color(0xFF267BE5), + sentMessage = Color(0x1E45B8FF), + receivedMessage = Color(0x20B1B0B5) +) + +val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) // Non-@Composable implementation private fun isInNightMode() = (SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES @Composable -fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.first.isLight +fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { @@ -64,14 +246,14 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { } val systemDark = isSystemInDarkTheme() LaunchedEffect(systemDark) { - if (CurrentColors.value.second == DefaultTheme.SYSTEM && CurrentColors.value.first.isLight == systemDark) { + if (SimplexApp.context.chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.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 = theme.first, + colors = theme.colors, 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 index a1d5ee4526..e6646c9bc2 100644 --- 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 @@ -7,22 +7,52 @@ import chat.simplex.app.R import chat.simplex.app.SimplexApp import chat.simplex.app.model.AppPreferences import chat.simplex.app.views.helpers.generalGetString +import okhttp3.internal.toHexString object ThemeManager { private val appPrefs: AppPreferences by lazy { - AppPreferences(SimplexApp.context) + SimplexApp.context.chatModel.controller.appPrefs } - 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) + data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) + + private fun systemDarkThemeColors(): Pair = when (appPrefs.systemDarkTheme.get()) { + DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK + DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX + else -> SimplexColorPalette to DefaultTheme.SIMPLEX + } + + fun currentColors(darkForSystemTheme: Boolean): ActiveTheme { + val themeName = appPrefs.currentTheme.get()!! + val themeOverrides = appPrefs.themeOverrides.get() + + val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + themeName + } else { + if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name } - return res.copy(first = res.first.copy(primary = Color(appPrefs.primaryColor.get()))) + val theme = themeOverrides[nonSystemThemeName] + val baseTheme = when (nonSystemThemeName) { + DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) + DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) + else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + } + if (theme == null) { + return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third) + } + return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) + } + + fun currentThemeOverrides(darkForSystemTheme: Boolean): ThemeOverrides { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + themeName + } else { + if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + } + val overrides = appPrefs.themeOverrides.get().toMutableMap() + return overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) } // colors, default theme enum, localized name of theme @@ -30,7 +60,7 @@ object ThemeManager { val allThemes = ArrayList>() allThemes.add( Triple( - if (darkForSystemTheme) DarkColorPalette else LightColorPalette, + if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette, DefaultTheme.SYSTEM, generalGetString(R.string.theme_system) ) @@ -49,16 +79,73 @@ object ThemeManager { generalGetString(R.string.theme_dark) ) ) + allThemes.add( + Triple( + SimplexColorPalette, + DefaultTheme.SIMPLEX, + generalGetString(R.string.theme_simplex) + ) + ) return allThemes } - fun applyTheme(name: String, darkForSystemTheme: Boolean) { - appPrefs.currentTheme.set(name) + fun applyTheme(theme: String, darkForSystemTheme: Boolean) { + appPrefs.currentTheme.set(theme) CurrentColors.value = currentColors(darkForSystemTheme) } - fun saveAndApplyPrimaryColor(color: Color) { - appPrefs.primaryColor.set(color.toArgb()) - CurrentColors.value = currentColors(!CurrentColors.value.first.isLight) + fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { + appPrefs.systemDarkTheme.set(theme) + CurrentColors.value = currentColors(darkForSystemTheme) + } + + fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + themeName + } else { + if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + } + var colorToSet = color + if (colorToSet == null) { + // Setting default color from a base theme + colorToSet = when(nonSystemThemeName) { + DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp) + DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp) + DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp) + // Will not be here + else -> return + } + } + val overrides = appPrefs.themeOverrides.get().toMutableMap() + val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) + overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex()) + appPrefs.themeOverrides.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + } + + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { + val overrides = appPrefs.themeOverrides.get().toMutableMap() + val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) + overrides[theme.base.name] = prevValue.copy(colors = theme.colors) + appPrefs.themeOverrides.set(overrides) + appPrefs.currentTheme.set(theme.base.name) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + } + + fun resetAllThemeColors(darkForSystemTheme: Boolean) { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + themeName + } else { + if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + } + val overrides = appPrefs.themeOverrides.get().toMutableMap() + val prevValue = overrides[nonSystemThemeName] ?: return + overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors()) + appPrefs.themeOverrides.set(overrides) + CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } } + +private fun Color.toReadableHex(): String = "#" + toArgb().toHexString() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt index f37a6fca8e..a831bcc3a9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -11,8 +10,8 @@ import androidx.compose.ui.Modifier fun SplashView() { Surface( Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize() + .fillMaxSize(), + color = MaterialTheme.colors.background ) { // Image( // painter = painterResource(R.drawable.logo), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 8641b4a08a..b342cc731d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -100,7 +100,7 @@ fun TerminalLayout( modifier = Modifier .padding(contentPadding) .fillMaxWidth() - .background(MaterialTheme.colors.background) + .themedBackground() ) { TerminalLog(terminalItems) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index cf9084ea1b..0eea155fb2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -101,7 +101,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { createColor = MaterialTheme.colors.primary } else { createModifier = Modifier.padding(8.dp) - createColor = HighOrLowlight + createColor = MaterialTheme.colors.secondary } Surface(shape = RoundedCornerShape(20.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { @@ -147,9 +147,9 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal derivedStateOf { if (valid) { if (focused) { - HighOrLowlight.copy(alpha = 0.6f) + CurrentColors.value.colors.secondary.copy(alpha = 0.6f) } else { - HighOrLowlight.copy(alpha = 0.3f) + CurrentColors.value.colors.secondary.copy(alpha = 0.3f) } } else Color.Red } @@ -176,7 +176,7 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal autoCorrect = false ), singleLine = true, - cursorBrush = SolidColor(HighOrLowlight) + cursorBrush = SolidColor(MaterialTheme.colors.secondary) ) } LaunchedEffect(Unit) { 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 9025ba32db..e7e235c606 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 @@ -316,7 +316,7 @@ private fun ActiveCallOverlayLayout( private fun ControlButton(call: Call, icon: Painter, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) { if (call.hasMedia) { IconButton(onClick = action, enabled = enabled) { - Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else HighOrLowlight, modifier = Modifier.size(40.dp)) + Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp)) } } else { Spacer(Modifier.size(40.dp)) @@ -367,7 +367,7 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { // horizontalAlignment = Alignment.CenterHorizontally, // verticalArrangement = Arrangement.spacedBy(12.dp), // modifier = Modifier -// .background(MaterialTheme.colors.background) +// .themedBackground() // .fillMaxSize() // ) { // WebRTCView(callCommand) { apiMsg -> diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt index 55abc1a941..1f8b0775c5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt @@ -97,7 +97,7 @@ fun IncomingCallActivityView(m: ChatModel) { SimpleXTheme { Surface( Modifier - .background(MaterialTheme.colors.background) + .themedBackground() .fillMaxSize()) { if (showCallView) { Box { @@ -212,7 +212,7 @@ private fun LockScreenCallButton(text: String, icon: Painter, color: Color, acti Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f)) } Spacer(Modifier.height(DEFAULT_PADDING)) - Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) + Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary) } } } @@ -226,7 +226,7 @@ fun PreviewIncomingCallLockScreenAlert() { SimpleXTheme(true) { Surface( Modifier - .background(MaterialTheme.colors.background) + .themedBackground() .fillMaxSize()) { IncomingCallLockScreenAlertLayout( invitation = RcvCallInvitation( 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 56d57e1443..c062f6f81b 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 @@ -72,7 +72,7 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) { @Composable fun CallIcon(icon: Painter, descr: String) = Icon(icon, descr, tint = SimplexGreen) Row(verticalAlignment = Alignment.CenterVertically) { if (chatModel.users.size > 1) { - ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondary) + ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondaryVariant) Spacer(Modifier.width(4.dp)) } if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call)) @@ -96,7 +96,7 @@ private fun CallButton(text: String, icon: Painter, color: Color, action: () -> horizontalAlignment = Alignment.CenterHorizontally ) { Icon(icon, text, tint = color, modifier = Modifier.scale(1.2f)) - Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) + Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary) } } } 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 12b21f9ddc..0474ff71cd 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 @@ -242,7 +242,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { if (contact.verified) { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary) } Text( contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), @@ -283,13 +283,13 @@ fun LocalAliasEditor( Text( generalGetString(R.string.text_field_set_contact_placeholder), textAlign = if (center) TextAlign.Center else TextAlign.Start, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) }, leadingIcon = if (leadingIcon) { { Icon(painterResource(R.drawable.ic_edit_filled), null, Modifier.padding(start = 7.dp)) } } else null, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, focus = focus, textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center), keyboardActions = KeyboardActions(onDone = { updateValue(value) }) @@ -336,7 +336,7 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) { ) { Text( networkStatus.statusString, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) ServerImage(networkStatus) } @@ -350,10 +350,10 @@ private fun ServerImage(networkStatus: NetworkStatus) { is NetworkStatus.Connected -> Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant) is NetworkStatus.Disconnected -> - Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary) is NetworkStatus.Error -> - Icon(painterResource(R.drawable.ic_error_filled), stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight) - else -> Icon(painterResource(R.drawable.ic_circle), stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_error_filled), stringResource(R.string.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary) + else -> Icon(painterResource(R.drawable.ic_circle), stringResource(R.string.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary) } } } @@ -381,7 +381,7 @@ fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) { if (contactVerified) painterResource(R.drawable.ic_verified_user) else painterResource(R.drawable.ic_shield), stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code), click = onClick, - iconColor = HighOrLowlight, + iconColor = MaterialTheme.colors.secondary, ) } 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 1731ba49a6..9b2da5a42c 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 @@ -326,7 +326,7 @@ fun ChatLayout( Box( Modifier .fillMaxWidth() - .background(MaterialTheme.colors.background) + .themedBackground() ) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( @@ -462,7 +462,7 @@ fun ChatInfoToolbar( } @Composable -fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondary) { +fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically @@ -496,7 +496,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo @Composable private fun ContactVerifiedShield() { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState) 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 a36b09a169..d90d2493ba 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 @@ -2,6 +2,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -10,16 +11,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.SentColorLight @Composable fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage Row( Modifier .height(60.dp) .fillMaxWidth() .padding(top = 8.dp) - .background(SentColorLight), + .background(sentColor), verticalAlignment = Alignment.CenterVertically ) { Icon( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt index 6823f29ce8..f84bee44c8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt @@ -14,21 +14,18 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.app.R -import chat.simplex.app.model.durationText -import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.views.chat.item.SentColorLight +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.UploadContent import chat.simplex.app.views.helpers.base64ToBitmap @Composable fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage Row( Modifier .padding(top = 8.dp) - .background(SentColorLight), + .background(sentColor), verticalAlignment = Alignment.CenterVertically, ) { LazyRow( 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 1840600666..9284e81c9f 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 @@ -33,7 +33,6 @@ import androidx.core.content.ContextCompat import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.helpers.* import kotlinx.coroutines.* @@ -689,7 +688,7 @@ fun ComposeView( Icon( painterResource(R.drawable.ic_attach_file_filled_500), contentDescription = stringResource(R.string.attach), - tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else HighOrLowlight, + tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier .size(28.dp) .clip(CircleShape) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt index f0994b870e..9c6babe65e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.durationText import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.SentColorLight import chat.simplex.app.views.helpers.* import kotlinx.coroutines.flow.distinctUntilChanged @@ -57,12 +56,13 @@ fun ComposeVoiceView( .height(3.dp) .background(MaterialTheme.colors.primary) ) + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage Row( Modifier .height(60.dp) .fillMaxWidth() .padding(top = 8.dp) - .background(SentColorLight), + .background(sentColor), verticalAlignment = Alignment.CenterVertically ) { IconButton( @@ -80,7 +80,7 @@ fun ComposeVoiceView( Modifier .padding(start = 4.dp, end = 2.dp) .size(36.dp), - tint = if (finishedRecording) MaterialTheme.colors.primary else HighOrLowlight + tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } val numberInText = remember(recordedDurationMs, progress.value) { @@ -95,7 +95,7 @@ fun ComposeVoiceView( Text( durationText(numberInText.value), fontSize = 18.sp, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, ) Spacer(Modifier.weight(1f)) if (cancelEnabled) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt index 46c96e47af..c5fa9daa57 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt @@ -194,10 +194,10 @@ private fun TimedMessagesFeatureSection( private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { SectionView { SectionItemView(reset, disabled = disabled) { - Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } SectionItemView(save, disabled = disabled) { - Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt index c030fb86d2..5a24eaf525 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -13,8 +14,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.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.item.* import kotlinx.datetime.Clock @@ -25,10 +25,12 @@ fun ContextItemView( cancelContextItem: () -> Unit ) { val sent = contextItem.chatDir.sent + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Row( Modifier .padding(top = 8.dp) - .background(if (sent) SentColorLight else ReceivedColorLight), + .background(if (sent) sentColor else receivedColor), verticalAlignment = Alignment.CenterVertically ) { Row( @@ -45,7 +47,7 @@ fun ContextItemView( .height(20.dp) .width(20.dp), contentDescription = stringResource(R.string.icon_descr_context), - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) MarkdownText( contextItem.text, contextItem.formattedText, 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 7b9b323cb6..d9d67074d3 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 @@ -44,7 +44,7 @@ import androidx.core.widget.* import chat.simplex.app.R import chat.simplex.app.SimplexApp import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.helpers.* @@ -187,7 +187,7 @@ private fun NativeKeyboard( ) { val cs = composeState.value val textColor = MaterialTheme.colors.onBackground - val tintColor = MaterialTheme.colors.secondary + val tintColor = MaterialTheme.colors.secondaryVariant val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } @@ -240,7 +240,7 @@ private fun NativeKeyboard( editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) editText.setText(cs.message) if (Build.VERSION.SDK_INT >= 29) { - editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) } + editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) } } else { try { val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes") @@ -284,7 +284,7 @@ private fun ComposeOverlay(textId: Int, textStyle: MutableState, padd Text( generalGetString(textId), Modifier.padding(padding), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, style = textStyle.value.copy(fontStyle = FontStyle.Italic) ) } @@ -295,7 +295,7 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState) { composeState.value = composeState.value.copy(message = "") }, Modifier.align(Alignment.TopEnd).size(36.dp) ) { - Icon(painterResource(R.drawable.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary) } } @@ -354,7 +354,7 @@ private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { Icon( painterResource(R.drawable.ic_keyboard_voice), stringResource(R.string.icon_descr_record_voice_message), - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, modifier = Modifier .size(36.dp) .padding(4.dp) @@ -424,7 +424,7 @@ private fun RecordVoiceButton(interactionSource: MutableInteractionSource) { @Composable private fun ProgressIndicator() { - CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp) + CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) } @Composable @@ -474,7 +474,7 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight) + .background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) .padding(3.dp) ) } @@ -497,7 +497,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { Icon( BoltFilled, stringResource(R.string.icon_descr_send_message), - tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight, + tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier .size(36.dp) .padding(4.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt index 81d20e8444..12e8ab1aa9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt @@ -65,7 +65,7 @@ private fun VerifyCodeLayout( val splitCode = splitToParts(connectionCode, 24) Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { if (connectionVerified) { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary) Text(String.format(stringResource(R.string.is_verified), displayName)) } else { Text(String.format(stringResource(R.string.is_not_verified), displayName)) 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 a57f563250..096b19de0a 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 @@ -133,7 +133,7 @@ fun AddGroupMembersLayout( Text( stringResource(R.string.no_contacts_to_add), Modifier.padding(), - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } else { @@ -214,7 +214,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec if (selectedContactsCount >= 1) { Text( String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 12.sp ) Box( @@ -222,14 +222,14 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec ) { Text( stringResource(R.string.clear_contacts_selection_button), - color = if (enabled) MaterialTheme.colors.primary else HighOrLowlight, + color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, fontSize = 12.sp ) } } else { Text( stringResource(R.string.no_contacts_selected), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 12.sp ) } @@ -270,13 +270,13 @@ fun ContactCheckRow( val iconColor: Color if (prohibitedToInviteIncognito) { icon = painterResource(R.drawable.ic_theater_comedy_filled) - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary } else if (checked) { icon = painterResource(R.drawable.ic_check_circle_filled) - iconColor = if (enabled) MaterialTheme.colors.primary else HighOrLowlight + iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary } else { icon = painterResource(R.drawable.ic_circle) - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary } SectionItemView( click = if (enabled) { @@ -294,7 +294,7 @@ fun ContactCheckRow( Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) Text( contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (prohibitedToInviteIncognito) HighOrLowlight else Color.Unspecified + color = if (prohibitedToInviteIncognito) MaterialTheme.colors.secondary else Color.Unspecified ) Spacer(Modifier.fillMaxWidth().weight(1f)) Icon( 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 7eb93e2d32..728056f1d9 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 @@ -191,7 +191,7 @@ fun GroupChatInfoLayout( } val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers - val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary + val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary AddMembersButton(tint, onAddMembersClick) } SectionItemView(minHeight = 54.dp) { @@ -305,7 +305,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) { val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s Text( statusDescr, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -314,14 +314,14 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) { } val role = member.memberRole if (role == GroupMemberRole.Owner || role == GroupMemberRole.Admin) { - Text(role.text, color = HighOrLowlight) + Text(role.text, color = MaterialTheme.colors.secondary) } } } @Composable private fun MemberVerifiedShield() { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary) } @Composable @@ -330,7 +330,7 @@ private fun GroupLinkButton(onClick: () -> Unit) { painterResource(R.drawable.ic_link), stringResource(R.string.group_link), onClick, - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary ) } @@ -340,7 +340,7 @@ private fun CreateGroupLinkButton(onClick: () -> Unit) { painterResource(R.drawable.ic_add_link), stringResource(R.string.create_group_link), onClick, - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary ) } @@ -350,7 +350,7 @@ fun EditGroupProfileButton(onClick: () -> Unit) { painterResource(R.drawable.ic_edit), stringResource(R.string.button_edit_group_profile), onClick, - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary ) } @@ -365,7 +365,7 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit painterResource(R.drawable.ic_maps_ugc), text, onClick, - iconColor = HighOrLowlight + iconColor = MaterialTheme.colors.secondary ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt index 6fb2eaf9d3..42a893ce99 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt @@ -4,8 +4,7 @@ import SectionBottomSpacer import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -177,7 +176,7 @@ fun ProgressIndicator() { Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } 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 612b017823..0ba92cc3fc 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 @@ -239,7 +239,7 @@ fun GroupMemberInfoHeader(member: GroupMember) { ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) Row(verticalAlignment = Alignment.CenterVertically) { if (member.verified) { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary) } Text( member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt index aa9715cf1a..2b2d85e2a3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt @@ -123,7 +123,7 @@ private fun FeatureSection( SectionView { val on = enableFeature.value == GroupFeatureEnabled.ON val icon = if (on) feature.iconFilled() else feature.icon - val iconTint = if (on) SimplexGreen else HighOrLowlight + val iconTint = if (on) SimplexGreen else MaterialTheme.colors.secondary val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON if (groupInfo.canEdit) { PreferenceToggleWithIcon( @@ -157,10 +157,10 @@ private fun FeatureSection( private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { SectionView { SectionItemView(reset, disabled = disabled) { - Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } SectionItemView(save, disabled = disabled) { - Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt index 844c0f149e..93e37c9e74 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt @@ -114,7 +114,7 @@ fun GroupProfileLayout( ) { Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f)) + ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) EditImageButton { scope.launch { bottomSheetModalState.show() } } } if (profileImage.value != null) { @@ -163,7 +163,7 @@ fun GroupProfileLayout( } else { Text( stringResource(R.string.save_group_profile), - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt index 906a8815e9..9e9408e993 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt @@ -79,7 +79,7 @@ private fun GroupWelcomeLayout( private fun SaveButton(save: () -> Unit, disabled: Boolean) { SectionView { SectionItemView(save, disabled = disabled) { - Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt index 06224d1cc0..378ee4d5b4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,15 +34,15 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat CICallStatus.Negotiated -> ConnectingCallIcon() CICallStatus.Progress -> Icon(painterResource(R.drawable.ic_phone_in_talk_filled), stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen) CICallStatus.Ended -> Row { - Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp)) - Text(durationText(duration), color = HighOrLowlight) + Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_ended), tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 4.dp)) + Text(durationText(duration), color = MaterialTheme.colors.secondary) } CICallStatus.Error -> {} } Text( cItem.timestampText, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 14.sp, modifier = Modifier.padding(start = 3.dp) ) @@ -55,7 +54,7 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) { if (cInfo is ChatInfo.Direct) { SimpleButton(stringResource(R.string.answer_call), painterResource(R.drawable.ic_ring_volume)) { acceptCall(cInfo.contact) } } else { - Icon(painterResource(R.drawable.ic_ring_volume), stringResource(R.string.answer_call), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_ring_volume), stringResource(R.string.answer_call), tint = MaterialTheme.colors.secondary) } // if case let .direct(contact) = chatInfo { // Button { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt index 5b37c402b7..32933a57d7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt @@ -13,8 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* @Composable fun CIEventView(ci: ChatItem) { @@ -22,28 +21,25 @@ fun CIEventView(ci: ChatItem) { fun chatEventTextView(text: AnnotatedString) { Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) } - - Surface { - Row( - Modifier.padding(horizontal = 6.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val memberDisplayName = ci.memberDisplayName - if (memberDisplayName != null) { - chatEventTextView( - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(ci)) - ) - } else { - chatEventTextView(chatEventText(ci)) - } + Row( + Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val memberDisplayName = ci.memberDisplayName + if (memberDisplayName != null) { + chatEventTextView( + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(ci)) + ) + } else { + chatEventTextView(chatEventText(ci)) } } } -val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight) +val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) fun chatEventText(ci: ChatItem): AnnotatedString = buildAnnotatedString { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt index bb3f400eab..ab9d0896a1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.unit.dp 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.views.helpers.generalGetString @Composable @@ -27,7 +26,7 @@ fun CIFeaturePreferenceView( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = HighOrLowlight) + Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = MaterialTheme.colors.secondary) if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) { val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp) val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null @@ -48,7 +47,7 @@ fun CIFeaturePreferenceView( ) } else { Text(chatItem.content.text + " " + chatItem.timestampText, - fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight) + fontSize = 12.sp, fontWeight = FontWeight.Light, color = MaterialTheme.colors.secondary) } } } 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 0194da14d8..39ac18f08a 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 @@ -194,7 +194,7 @@ fun CIFileView( ) Text( formatBytes(file.fileSize) + metaReserve, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 14.sp, maxLines = 1 ) 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 d55c4ee8ba..e53af51af0 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 @@ -8,7 +8,6 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -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 @@ -69,12 +68,14 @@ fun CIGroupInvitationView( } } + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( modifier = if (action) Modifier.clickable(onClick = { joinGroup(groupInvitation.groupId) }) else Modifier, shape = RoundedCornerShape(18.dp), - color = if (sent) SentColorLight else ReceivedColorLight, + color = if (sent) sentColor else receivedColor, ) { Box( Modifier @@ -105,7 +106,7 @@ fun CIGroupInvitationView( } Text( ci.timestampText, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 14.sp, modifier = Modifier.padding(start = 3.dp) ) 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 81afbc10e4..23ed293876 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 @@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.CurrentColors import kotlinx.datetime.Clock @Composable -fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) { +fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = MaterialTheme.colors.secondary) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { Text( @@ -72,7 +72,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String { res += TimedMessagesPreference.shortTtlText(ttl) } } - if (meta.statusIcon(HighOrLowlight) != null || !meta.disappearing) { + if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) { res += iconSpace } return res + meta.timestampText diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt index 8d6bd2c8cc..d8acbe6c49 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt @@ -137,7 +137,7 @@ private fun DurationText(text: State, padding: PaddingValues) { Modifier .padding(padding) .widthIn(min = minWidth), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, fontSize = 16.sp, maxLines = 1 ) @@ -156,9 +156,11 @@ private fun PlayPauseButton( pause: () -> Unit, longClick: () -> Unit ) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( Modifier.drawRingModifier(angle, strokeColor, strokeWidth), - color = if (sent) SentColorLight else ReceivedColorLight, + color = if (sent) sentColor else receivedColor, shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) ) { Box( @@ -174,7 +176,7 @@ private fun PlayPauseButton( if (audioPlaying) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled), contentDescription = null, Modifier.size(36.dp), - tint = if (error) WarningOrange else if (!enabled) HighOrLowlight else MaterialTheme.colors.primary + tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } } 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 96c6fbca76..f5376355f4 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 @@ -247,7 +247,7 @@ fun ChatItemView( val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) } - is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,) + is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,) is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt index 06499418b1..49a9ce28e6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.* @@ -13,15 +14,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* @Composable fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { val sent = ci.chatDir.sent + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), - color = if (sent) SentColorLight else ReceivedColorLight, + color = if (sent) sentColor else receivedColor, ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), @@ -30,7 +32,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = Text( buildAnnotatedString { appendSender(this, if (showMember) ci.memberDisplayName else null, true) - withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(ci.content.text) } + withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) 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 fe80af7bc5..99d6ab7eae 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 @@ -29,11 +29,6 @@ import chat.simplex.app.views.helpers.* import kotlinx.datetime.Clock import kotlin.math.min -val SentColorLight = Color(0x1E45B8FF) -val ReceivedColorLight = Color(0x20B1B0B5) -val SentQuoteColorLight = Color(0x2545B8FF) -val ReceivedQuoteColorLight = Color(0x25B1B0B5) - @Composable fun FramedItemView( chatInfo: ChatInfo, @@ -54,6 +49,9 @@ fun FramedItemView( return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null } + @Composable + fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) + @Composable fun ciQuotedMsgView(qi: CIQuote) { Box( @@ -70,9 +68,11 @@ fun FramedItemView( @Composable fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Row( Modifier - .background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) + .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) .fillMaxWidth() .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -87,7 +87,7 @@ fun FramedItemView( } Text( buildAnnotatedString { - withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = HighOrLowlight)) { + withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = MaterialTheme.colors.secondary)) { append(caption) } }, @@ -100,9 +100,11 @@ fun FramedItemView( @Composable fun ciQuoteView(qi: CIQuote) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Row( Modifier - .background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) + .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) .fillMaxWidth() .combinedClickable( onLongClick = { showMenu.value = true }, @@ -163,16 +165,18 @@ fun FramedItemView( val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Box(Modifier .clip(RoundedCornerShape(18.dp)) .background( when { transparentBackground -> Color.Transparent - sent -> SentColorLight - else -> ReceivedColorLight + sent -> sentColor + else -> receivedColor } )) { - var metaColor = HighOrLowlight + var metaColor = MaterialTheme.colors.secondary Box(contentAlignment = Alignment.BottomEnd) { Column(Modifier.width(IntrinsicSize.Max)) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt index fa25e0f88a..e1ddd3b3e7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -18,6 +19,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatItem import chat.simplex.app.model.MsgErrorType +import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.AlertManager import chat.simplex.app.views.helpers.generalGetString @@ -50,10 +52,11 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT @Composable fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) { + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), - color = ReceivedColorLight, + color = receivedColor, ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt index a63a232939..4bb56b60fa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.* @@ -16,15 +17,16 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.CIDeleted import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.generalGetString @Composable fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), - color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight, + color = if (ci.chatDir.sent) sentColor else receivedColor, ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), @@ -47,7 +49,7 @@ private fun MarkedDeletedText(text: String) { Text( buildAnnotatedString { // appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size - withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(text) } + withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index d6fe1b2f4c..de5fd1594e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.* import androidx.core.text.BidiFormatter import chat.simplex.app.TAG import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.app.views.helpers.detectGesture import kotlinx.coroutines.* @@ -57,7 +57,7 @@ private val typingIndicators: List = listOf( private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString { - pushStyle(SpanStyle(color = HighOrLowlight, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp)) + pushStyle(SpanStyle(color = CurrentColors.value.colors.secondary, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp)) append(if (recent) typingIndicators[typingIdx] else noTyping) } 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 fa8b639b96..e005ae0c3d 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 @@ -328,7 +328,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: @Composable private fun InvalidDataView() { Row { - ProfileImage(72.dp, null, R.drawable.ic_account_circle_filled, HighOrLowlight) + ProfileImage(72.dp, null, R.drawable.ic_account_circle_filled, MaterialTheme.colors.secondary) Column( modifier = Modifier .padding(horizontal = 8.dp) 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 1580a265d2..6104f7e6b0 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 @@ -84,7 +84,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) hoveredElevation = 0.dp, focusedElevation = 0.dp, ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else HighOrLowlight, + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, contentColor = Color.White ) { Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(R.drawable.ic_edit_filled) else painterResource(R.drawable.ic_close), stringResource(R.string.add_contact_or_create_group)) @@ -96,7 +96,6 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) Column( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colors.background) ) { if (chatModel.chats.isNotEmpty()) { ChatList(chatModel, search = searchInList) @@ -105,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { OnboardingButtons(showNewChatSheet) } - Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight) + Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) } } } @@ -136,7 +135,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) { } Spacer(Modifier.height(DEFAULT_PADDING)) ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet) - val color = MaterialTheme.colors.primary + val color = MaterialTheme.colors.primaryVariant Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = { val trianglePath = Path().apply { moveTo(0.dp.toPx(), 0f) @@ -159,7 +158,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { onClick, shape = RoundedCornerShape(21.dp), colors = ButtonDefaults.textButtonColors( - backgroundColor = MaterialTheme.colors.primary + backgroundColor = MaterialTheme.colors.primaryVariant ), elevation = null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), @@ -282,7 +281,7 @@ private fun ProgressIndicator() { Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } 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 1c641f696a..5a4745ced8 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 @@ -46,7 +46,7 @@ fun ChatPreviewView( painterResource(R.drawable.ic_cancel_filled), stringResource(R.string.icon_descr_group_inactive), Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), - tint = HighOrLowlight + tint = MaterialTheme.colors.secondary ) } @@ -76,7 +76,7 @@ fun ChatPreviewView( @Composable fun VerifiedIcon() { - Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } fun messageDraft(draft: ComposeState): Pair> { @@ -110,7 +110,7 @@ fun ChatPreviewView( "attachmentIcon" to InlineTextContent( Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) ) { - Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(R.drawable.ic_edit_note), null, tint = HighOrLowlight) + Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(R.drawable.ic_edit_note), null, tint = MaterialTheme.colors.secondary) } ) return text to inlineContent @@ -124,12 +124,12 @@ fun ChatPreviewView( if (cInfo.contact.verified) { VerifiedIcon() } - chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight) + chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else MaterialTheme.colors.secondary) } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary) - GroupMemberStatus.MemAccepted -> chatPreviewTitleText(HighOrLowlight) + GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) else -> chatPreviewTitleText() } else -> chatPreviewTitleText() @@ -170,12 +170,12 @@ fun ChatPreviewView( when (cInfo) { is ChatInfo.Direct -> if (!cInfo.ready) { - Text(stringResource(R.string.contact_connection_pending), color = HighOrLowlight) + Text(stringResource(R.string.contact_connection_pending), color = MaterialTheme.colors.secondary) } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo)) - GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = HighOrLowlight) + GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = MaterialTheme.colors.secondary) else -> {} } else -> {} @@ -208,7 +208,7 @@ fun ChatPreviewView( ) { Text( ts, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(bottom = 5.dp) ) @@ -221,10 +221,10 @@ fun ChatPreviewView( ) { Text( if (n > 0) unreadCountStr(n) else "", - color = MaterialTheme.colors.onPrimary, + color = Color.White, fontSize = 11.sp, modifier = Modifier - .background(if (stopped || showNtfsIcon) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) + .background(if (stopped || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) .badgeLayout() .padding(horizontal = 3.dp) .padding(vertical = 1.dp) @@ -238,7 +238,7 @@ fun ChatPreviewView( Icon( painterResource(R.drawable.ic_notifications_off_filled), contentDescription = generalGetString(R.string.notifications), - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, modifier = Modifier .padding(horizontal = 3.dp) .padding(vertical = 1.dp) @@ -280,7 +280,7 @@ fun ChatStatusImage(s: NetworkStatus?) { Icon( painterResource(R.drawable.ic_error), contentDescription = descr, - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, modifier = Modifier .size(19.dp) ) @@ -289,7 +289,7 @@ fun ChatStatusImage(s: NetworkStatus?) { Modifier .padding(horizontal = 2.dp) .size(15.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 1.5.dp ) } 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 9e4cd20cad..de6fe43d50 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 @@ -34,7 +34,7 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) val height = with(LocalDensity.current) { 46.sp.toDp() } Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) @@ -45,7 +45,7 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { ) { Text( ts, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(bottom = 5.dp) ) 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 064cb4a66a..936d3d2590 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 @@ -42,7 +42,7 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con ) { Text( ts, - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(bottom = 5.dp) ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt index f601e65378..0be8f9b531 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt @@ -19,8 +19,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.Indigo +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import kotlinx.coroutines.flow.MutableStateFlow @@ -36,7 +35,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { Column( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colors.background) + .themedBackground() ) { if (chatModel.chats.isNotEmpty()) { ShareList(chatModel, search = searchInList) @@ -54,7 +53,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { @Composable private fun EmptyList() { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.you_have_no_chats), color = HighOrLowlight) + Text(stringResource(R.string.you_have_no_chats), color = MaterialTheme.colors.secondary) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt index d073d2d2de..31d6e96efa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt @@ -107,7 +107,7 @@ fun UserPicker( .width(IntrinsicSize.Min) .height(IntrinsicSize.Min) .shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true) - .background(if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp))) + .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp))) .clip(RoundedCornerShape(corner = CornerSize(25.dp))) ) { Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { @@ -169,7 +169,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues if (u.activeUser) { Icon(painterResource(R.drawable.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) } else if (u.hidden) { - Icon(painterResource(R.drawable.ic_lock), null, Modifier.size(20.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } else if (unreadCount > 0) { Box( contentAlignment = Alignment.Center @@ -179,13 +179,13 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues color = Color.White, fontSize = 11.sp, modifier = Modifier - .background(if (u.showNtfs) MaterialTheme.colors.primary else HighOrLowlight, shape = CircleShape) + .background(MaterialTheme.colors.primaryVariant, shape = CircleShape) .padding(2.dp) .badgeLayout() ) } } else if (!u.showNtfs) { - Icon(painterResource(R.drawable.ic_notifications_off), null, Modifier.size(20.dp), tint = HighOrLowlight) + Icon(painterResource(R.drawable.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } else { Box(Modifier.size(20.dp)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt index 5c24e59bd4..8923962bcd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt @@ -112,7 +112,7 @@ fun DatabaseEncryptionView(m: ChatModel) { Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } @@ -211,7 +211,7 @@ fun DatabaseEncryptionLayout( ) SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } @@ -292,7 +292,7 @@ fun SavePassphraseSetting( Icon( if (storedKey) painterResource(R.drawable.ic_vpn_key_filled) else painterResource(R.drawable.ic_vpn_key_off_filled), stringResource(R.string.save_passphrase_in_keychain), - tint = if (storedKey) SimplexGreen else HighOrLowlight + tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -360,7 +360,7 @@ fun PassphraseField( if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled) } else painterResource(R.drawable.ic_error) val iconColor = if (valid) { - if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else HighOrLowlight + if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else MaterialTheme.colors.secondary } else Color.Red val keyboard = LocalSoftwareKeyboardController.current val keyboardOptions = KeyboardOptions( @@ -417,7 +417,7 @@ fun PassphraseField( TextFieldDefaults.TextFieldDecorationBox( value = state.value.text, innerTextField = innerTextField, - placeholder = { Text(placeholder, color = HighOrLowlight) }, + placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) }, singleLine = true, enabled = enabled, isError = !valid, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 410bdea630..c1aa1fe5a6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -180,7 +180,7 @@ fun DatabaseErrorView( Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index 5feaa4bdff..2a74524b3c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -114,7 +114,7 @@ fun DatabaseView( Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } @@ -185,7 +185,7 @@ fun DatabaseLayout( .drawable.ic_lock), stringResource(R.string.database_passphrase), click = showSettingsModal() { DatabaseEncryptionView(it) }, - iconColor = if (unencrypted) WarningOrange else HighOrLowlight, + iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) AppDataBackupPreference(privacyFullBackup, initialRandomDBPassphrase) @@ -250,7 +250,7 @@ fun DatabaseLayout( ) { Text( stringResource(if (users.size > 1) R.string.delete_files_and_media_for_all_users else R.string.delete_files_and_media_all), - color = if (deleteFilesDisabled) HighOrLowlight else Color.Red + color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red ) } } @@ -270,7 +270,7 @@ fun DatabaseLayout( private fun AppDataBackupPreference(privacyFullBackup: SharedPreference, initialRandomDBPassphrase: SharedPreference) { SettingsPreferenceItem( painterResource(R.drawable.ic_backup), - iconColor = HighOrLowlight, + iconColor = MaterialTheme.colors.secondary, pref = privacyFullBackup, text = stringResource(R.string.full_backup) ) { 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 b12957f8d1..f4895c22ce 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 @@ -37,7 +37,6 @@ class AlertManager { ) { showAlert { AlertDialog( - backgroundColor = if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, onDismissRequest = this::hideAlert, title = alertTitle(title), text = alertText(text), @@ -56,7 +55,7 @@ class AlertManager { Dialog(onDismissRequest = this::hideAlert) { Column( Modifier - .background(if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp))) + .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp))) .padding(bottom = DEFAULT_PADDING) ) { Text( @@ -67,7 +66,7 @@ class AlertManager { ) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { if (text != null) { - Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = HighOrLowlight) + Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary) } buttons() } @@ -106,7 +105,6 @@ class AlertManager { }) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } }, - backgroundColor = if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -142,7 +140,6 @@ class AlertManager { }) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) } } }, - backgroundColor = if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -167,7 +164,6 @@ class AlertManager { }) { Text(confirmText, color = Color.Unspecified) } } }, - backgroundColor = if (isInDarkTheme()) DarkGrayBackground else MaterialTheme.colors.background, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -209,7 +205,7 @@ private fun alertText(text: String?): (@Composable () -> Unit)? { Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 16.sp, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) }) } 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 04d2744484..d8463d94a7 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 @@ -20,7 +20,7 @@ import chat.simplex.app.model.ChatInfo import chat.simplex.app.ui.theme.SimpleXTheme @Composable -fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondary) { +fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { val icon = if (chatInfo is ChatInfo.Group) R.drawable.ic_supervised_user_circle_filled else R.drawable.ic_account_circle_filled @@ -28,7 +28,7 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme } @Composable -fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary) { +fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { Box(Modifier.size(size)) { Icon( painterResource(R.drawable.ic_theater_comedy_filled), stringResource(R.string.incognito), @@ -43,7 +43,7 @@ fun ProfileImage( size: Dp, image: String? = null, icon: Int = R.drawable.ic_account_circle_filled, - color: Color = MaterialTheme.colors.secondary + color: Color = MaterialTheme.colors.secondaryVariant ) { Box(Modifier.size(size)) { if (image == null) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index 1c9cb170d0..6ee9b62a05 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -3,9 +3,12 @@ package chat.simplex.app.views.helpers import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -41,14 +44,20 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U @Composable fun AppBarTitle(title: String, withPadding: Boolean = true) { + val theme = CurrentColors.collectAsState() + val titleColor = CurrentColors.collectAsState().value.appColors.title + val brush = if (theme.value.base == DefaultTheme.SIMPLEX) + Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + else // color is not updated when changing themes if I pass null here + Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) Text( title, Modifier .fillMaxWidth() .padding(bottom = DEFAULT_PADDING * 1.5f, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1, - color = MaterialTheme.colors.primary, + style = MaterialTheme.typography.h1.copy(brush = brush), + color = MaterialTheme.colors.primaryVariant, textAlign = TextAlign.Center ) } 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 index e3d4048cfa..2ea147014f 100644 --- 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R -import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.database.PassphraseStrength import chat.simplex.app.views.database.validKey import kotlinx.coroutines.delay @@ -137,7 +136,7 @@ fun DefaultConfigurableTextField( if (showKey) painterResource(R.drawable.ic_visibility_off_filled) else painterResource(R.drawable.ic_visibility_filled) } else painterResource(R.drawable.ic_error) val iconColor = if (valid) { - if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else HighOrLowlight + if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else MaterialTheme.colors.secondary } else Color.Red val keyboard = LocalSoftwareKeyboardController.current val keyboardOptions = KeyboardOptions( @@ -189,7 +188,7 @@ fun DefaultConfigurableTextField( TextFieldDefaults.TextFieldDecorationBox( value = state.value.text, innerTextField = innerTextField, - placeholder = { Text(placeholder, color = HighOrLowlight) }, + placeholder = { Text(placeholder, color = MaterialTheme.colors.secondary) }, singleLine = true, enabled = enabled, isError = !valid, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt index e6ec87e14d..71af738bda 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.helpers +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,7 +20,6 @@ fun DefaultDropdownMenu( dropdownMenuItems: (@Composable () -> Unit)? ) { MaterialTheme( - colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF0B0B0B) else MaterialTheme.colors.background), shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp))) ) { DropdownMenu( @@ -27,6 +27,7 @@ fun DefaultDropdownMenu( onDismissRequest = { showMenu.value = false }, Modifier .widthIn(min = 250.dp) + .background(MaterialTheme.colors.surface) .padding(vertical = 4.dp), offset = offset, ) { @@ -42,11 +43,13 @@ fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu( dropdownMenuItems: (@Composable () -> Unit)? ) { MaterialTheme( - colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background), shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp))) ) { ExposedDropdownMenu( - modifier = Modifier.widthIn(min = 200.dp).then(modifier), + modifier = Modifier + .widthIn(min = 200.dp) + .background(MaterialTheme.colors.surface) + .then(modifier), expanded = expanded.value, onDismissRequest = { expanded.value = false diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt index 251cb8df30..613caed54d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import chat.simplex.app.ui.theme.HighOrLowlight @Composable fun DefaultSwitch( @@ -20,12 +19,12 @@ fun DefaultSwitch( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: SwitchColors = SwitchDefaults.colors( checkedThumbColor = MaterialTheme.colors.primary, - uncheckedThumbColor = HighOrLowlight, + uncheckedThumbColor = MaterialTheme.colors.secondary, checkedTrackAlpha = 0.0f, uncheckedTrackAlpha = 0.0f, ) ) { - val color = if (checked) MaterialTheme.colors.primary.copy(alpha = 0.3f) else HighOrLowlight.copy(alpha = 0.3f) + val color = if (checked) MaterialTheme.colors.primary.copy(alpha = 0.3f) else MaterialTheme.colors.secondary.copy(alpha = 0.3f) val size = with(LocalDensity.current) { Size(46.dp.toPx(), 28.dp.toPx()) } val offset = with(LocalDensity.current) { Offset(1.dp.toPx(), 10.dp.toPx()) } val radius = with(LocalDensity.current) { 28.dp.toPx() } 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 225dd393ba..9896b0302e 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 @@ -47,7 +47,7 @@ fun DefaultTopAppBar( fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) { IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { Icon( - painterResource(R.drawable.ic_arrow_back_ios_new), stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else HighOrLowlight + painterResource(R.drawable.ic_arrow_back_ios_new), stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt index 7fa87a482c..b39b36e365 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt @@ -23,7 +23,7 @@ fun ExposedDropDownSettingRow( selection: State, label: String? = null, icon: Painter? = null, - iconTint: Color = HighOrLowlight, + iconTint: Color = MaterialTheme.colors.secondary, enabled: State = mutableStateOf(true), onSelected: (T) -> Unit ) { @@ -46,13 +46,13 @@ fun ExposedDropDownSettingRow( Modifier.widthIn(max = maxWidth), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) Spacer(Modifier.size(12.dp)) Icon( if (!expanded.value) painterResource(R.drawable.ic_expand_more) else painterResource(R.drawable.ic_expand_less), generalGetString(R.string.icon_descr_more_button), - tint = HighOrLowlight + tint = MaterialTheme.colors.secondary ) } DefaultExposedDropdownMenu( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt index 7624665036..f6c749b554 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap @@ -19,9 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.LinkPreview -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.chat.item.SentColorLight +import chat.simplex.app.ui.theme.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jsoup.Jsoup @@ -79,8 +78,9 @@ suspend fun getLinkPreview(url: String): LinkPreview? { @Composable fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage Row( - Modifier.fillMaxWidth().padding(top = 8.dp).background(SentColorLight), + Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor), verticalAlignment = Alignment.CenterVertically ) { if (linkPreview == null) { @@ -90,7 +90,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) { ) { CircularProgressIndicator( Modifier.size(16.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.dp ) } @@ -134,7 +134,7 @@ fun ChatItemLinkView(linkPreview: LinkPreview) { if (linkPreview.description != "") { Text(linkPreview.description, maxLines = 12, overflow = TextOverflow.Ellipsis, fontSize = 14.sp, lineHeight = 20.sp) } - Text(linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 12.sp, color = HighOrLowlight) + Text(linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 12.sp, color = MaterialTheme.colors.secondary) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index 7e834806a1..3ba958a540 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.app.TAG import chat.simplex.app.ui.theme.isInDarkTheme +import chat.simplex.app.ui.theme.themedBackground import java.util.concurrent.atomic.AtomicBoolean @Composable @@ -24,7 +25,7 @@ fun ModalView( ) { BackHandler(onBack = close) Surface(Modifier.fillMaxSize()) { - Column(Modifier.background(background)) { + Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { CloseSheetBar(close, endButtons) Box(modifier) { content() } } 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 2a50888e8e..38b00dadee 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 @@ -25,7 +25,7 @@ fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), Column { if (title != null) { Text( - title, color = HighOrLowlight, style = MaterialTheme.typography.body2, + title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp ) } @@ -37,7 +37,7 @@ fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), fun SectionView( title: String, icon: Painter, - iconTint: Color = HighOrLowlight, + iconTint: Color = MaterialTheme.colors.secondary, leadingIcon: Boolean = false, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit) @@ -46,7 +46,7 @@ fun SectionView( val iconSize = with(LocalDensity.current) { 21.sp.toDp() } Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) - Text(title, color = HighOrLowlight, style = MaterialTheme.typography.body2, fontSize = 12.sp) + Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp) if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) } Column(Modifier.padding(padding).fillMaxWidth()) { content() } @@ -147,7 +147,7 @@ fun SectionItemWithValue( values: List>, label: String? = null, icon: Painter? = null, - iconTint: Color = HighOrLowlight, + iconTint: Color = MaterialTheme.colors.secondary, enabled: State = mutableStateOf(true), onSelected: () -> Unit ) { @@ -161,7 +161,7 @@ fun SectionItemWithValue( values.first { it.value == currentValue.value }.title + (if (label != null) " $label" else ""), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } @@ -177,7 +177,7 @@ fun SectionTextFooter(text: AnnotatedString) { Text( text, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, lineHeight = 18.sp, fontSize = 14.sp ) @@ -228,10 +228,10 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color SectionItemViewSpaceBetween { Row { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } - if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: HighOrLowlight) + if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary) Text(title) } - Text(value, color = HighOrLowlight) + Text(value, color = MaterialTheme.colors.secondary) } } @@ -247,7 +247,7 @@ fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { .widthIn(max = (configuration.screenWidthDp / 2).dp), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index 9f32850e95..779f51d9e3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -1,14 +1,15 @@ package chat.simplex.app.ui.theme +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.ui.res.painterResource import androidx.compose.runtime.Composable 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.painter.Painter import androidx.compose.ui.text.font.FontWeight @@ -54,10 +55,10 @@ fun SimpleButton( ) { SimpleButtonFrame(click, disabled = disabled) { Icon( - icon, text, tint = if (disabled) HighOrLowlight else color, + icon, text, tint = if (disabled) MaterialTheme.colors.secondary else color, modifier = Modifier.padding(end = 8.dp) ) - Text(text, style = MaterialTheme.typography.caption, color = if (disabled) HighOrLowlight else color) + Text(text, style = MaterialTheme.typography.caption, color = if (disabled) MaterialTheme.colors.secondary else color) } } @@ -79,7 +80,7 @@ fun SimpleButtonIconEnded( @Composable fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable () -> Unit) { - Surface(shape = RoundedCornerShape(20.dp)) { + Box(Modifier.clip(RoundedCornerShape(20.dp))) { val modifier = if (disabled) modifier else modifier.clickable { click() } Row( verticalAlignment = Alignment.CenterVertically, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt index 71f9874eed..e00f9ba454 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.* import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.HighOrLowlight @Composable fun TextEditor( @@ -41,11 +40,11 @@ fun TextEditor( autoCorrect = false ), modifier = modifier, - cursorBrush = SolidColor(HighOrLowlight), + cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> Surface( shape = if (border) RoundedCornerShape(10.dp) else RectangleShape, - border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondary) else null + border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant) else null ) { Row( Modifier.background(background), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 0a274d20b9..39f7d88932 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -37,6 +37,8 @@ import androidx.core.text.HtmlCompat import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.ThemeOverrides +import com.charleskorn.kaml.decodeFromStream import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -388,6 +390,22 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable } } +fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverrides? { + SimplexApp.context.contentResolver.openInputStream(uri).use { + runCatching { + return yaml.decodeFromStream(it!!) + }.onFailure { + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.import_theme_error), + text = generalGetString(R.string.import_theme_error_desc), + ) + } + } + } + return null +} + fun saveImage(context: Context, uri: Uri): String? { val bitmap = getBitmapFromUri(uri) ?: return null return saveImage(context, bitmap) @@ -566,6 +584,9 @@ fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true fun Color.darker(factor: Float = 0.1f): Color = Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) +fun Color.lighter(factor: Float = 0.1f): Color = + Color(min(red * (1 + factor), 1f), min(green * (1 + factor), 1f), min(blue * (1 + factor), 1f), alpha) + fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT) fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt index 20ee2fbcde..0f6f063082 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* -import chat.simplex.app.ui.theme.HighOrLowlight @Composable fun PasscodeEntry( @@ -129,7 +128,7 @@ private fun PasswordDigit(size: Dp, d: Int, password: MutableState) { fontSize = 30.sp, letterSpacing = (-0.5).sp ), - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } @@ -137,7 +136,7 @@ private fun PasswordDigit(size: Dp, d: Int, password: MutableState) { @Composable private fun PasswordEdit(size: Dp, image: Painter, action: () -> Unit) { PasswordButton(size, action) { - Icon(image, null, tint = HighOrLowlight) + Icon(image, null, tint = MaterialTheme.colors.secondary) } } 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 b307ea62d9..5ad769adb6 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 @@ -63,7 +63,7 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit) .size(36.dp) .padding(4.dp) .align(Alignment.CenterHorizontally), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 3.dp ) } @@ -113,7 +113,7 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean Icon( painterResource(R.drawable.ic_info), stringResource(R.string.incognito), - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 10.dp).size(20.dp) ) Text(offText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2) 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 d8acc440bf..a570ca3411 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 @@ -141,7 +141,7 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { } .padding(8.dp)) } else { - CreateGroupButton(HighOrLowlight, Modifier.padding(8.dp)) + CreateGroupButton(MaterialTheme.colors.secondary, Modifier.padding(8.dp)) } LaunchedEffect(Unit) { delay(300) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt index ba9c8fc25d..18a149eb52 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt @@ -4,12 +4,12 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.HighOrLowlight enum class ConnectViaLinkTab { SCAN, PASTE @@ -44,7 +44,7 @@ fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) { } TabRow( selectedTabIndex = selection.value.ordinal, - backgroundColor = MaterialTheme.colors.background, + backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.primary, ) { tabTitles.forEachIndexed { index, it -> @@ -62,7 +62,7 @@ fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) { ) }, selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = HighOrLowlight, + unselectedContentColor = MaterialTheme.colors.secondary, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt index 6e4c6849d4..90e58a6986 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt @@ -5,13 +5,13 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.helpers.ModalManager import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.usersettings.UserAddressView @@ -67,7 +67,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { } TabRow( selectedTabIndex = selection.value.ordinal, - backgroundColor = MaterialTheme.colors.background, + backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.primary, ) { tabTitles.forEachIndexed { index, it -> @@ -84,7 +84,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { ) }, selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = HighOrLowlight, + unselectedContentColor = MaterialTheme.colors.secondary, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index c0214eb3cb..5224a3ab40 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -150,7 +150,7 @@ private fun NewChatSheetLayout( hoveredElevation = 0.dp, focusedElevation = 0.dp, ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else HighOrLowlight, + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, contentColor = Color.White ) { Icon( @@ -180,7 +180,7 @@ fun ActionButton( .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - val tint = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + val tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary Icon( icon, text, tint = tint, @@ -226,7 +226,7 @@ fun ActionButton( .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - val tint = if (disabled) HighOrLowlight else tint + val tint = if (disabled) MaterialTheme.colors.secondary else tint Icon( icon, text, tint = tint, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt index 774a962c42..a0d2c66b11 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt @@ -53,7 +53,7 @@ fun SetNotificationsMode(m: ChatModel) { private fun NotificationButton(currentMode: MutableState, mode: NotificationsMode, @StringRes title: Int, @StringRes description: Int) { TextButton( onClick = { currentMode.value = mode }, - border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)), + border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), shape = RoundedCornerShape(35.dp), ) { Column(Modifier.padding(horizontal = 14.dp).padding(top = 4.dp, bottom = 8.dp)) { @@ -61,7 +61,7 @@ private fun NotificationButton(currentMode: MutableState, mod stringResource(title), style = MaterialTheme.typography.h2, fontWeight = FontWeight.Medium, - color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight, + color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt index a3acb2e34b..1e7c8d6b22 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt @@ -1,6 +1,7 @@ package chat.simplex.app.views.onboarding import android.content.res.Configuration +import android.os.Build import androidx.annotation.IntegerRes import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -9,6 +10,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource @@ -18,6 +20,7 @@ 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 +import chat.simplex.app.BuildConfig import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.* @@ -47,7 +50,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 4.dp) ) { - Icon(icon, stringResource(titleId), tint = HighOrLowlight) + Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.secondary) Text( generalGetString(titleId), maxLines = 1, @@ -71,7 +74,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ) { if (currentVersion.value > 0) { val prev = currentVersion.value - 1 - Surface(shape = RoundedCornerShape(20.dp)) { + Box(Modifier.clip(RoundedCornerShape(20.dp))) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -87,7 +90,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { Spacer(Modifier.fillMaxWidth().weight(1f)) if (currentVersion.value < versionDescriptions.lastIndex) { val next = currentVersion.value + 1 - Surface(shape = RoundedCornerShape(20.dp)) { + Box(Modifier.clip(RoundedCornerShape(20.dp))) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -113,17 +116,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) ) { - Text( - String.format(generalGetString(R.string.new_in_version), v.version), - Modifier - .fillMaxWidth() - .padding(DEFAULT_PADDING), - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1, - fontWeight = FontWeight.Normal, - color = HighOrLowlight - ) + AppBarTitle(String.format(generalGetString(R.string.new_in_version), v.version)) v.features.forEach { feature -> featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt index 4c5ff2fd85..c383370042 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt @@ -60,7 +60,7 @@ private fun AcceptRequestsLayout( PreferenceToggleWithIcon( stringResource(R.string.incognito), if (autoAcceptState.value.incognito) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_theater_comedy), - if (autoAcceptState.value.incognito) Indigo else HighOrLowlight, + if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary, autoAcceptState.value.incognito, ) { autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText) @@ -81,7 +81,7 @@ private fun AcceptRequestsLayout( Spacer(Modifier.height(DEFAULT_PADDING)) if (autoAcceptState.value.enable) { Text( - stringResource(R.string.section_title_welcome_message), color = HighOrLowlight, style = MaterialTheme.typography.body2, + stringResource(R.string.section_title_welcome_message), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp ) TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt index fe1f29802d..044260ac83 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt @@ -184,13 +184,13 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { } } else { SectionItemView { - Text("TCP_KEEPIDLE", color = HighOrLowlight) + Text("TCP_KEEPIDLE", color = MaterialTheme.colors.secondary) } SectionItemView { - Text("TCP_KEEPINTVL", color = HighOrLowlight) + Text("TCP_KEEPINTVL", color = MaterialTheme.colors.secondary) } SectionItemView { - Text("TCP_KEEPCNT", color = HighOrLowlight) + Text("TCP_KEEPCNT", color = MaterialTheme.colors.secondary) } } } @@ -208,7 +208,7 @@ fun ResetToDefaultsButton(reset: () -> Unit, disabled: Boolean) { modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically ) { - val color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + val color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary Text(stringResource(R.string.network_options_reset_to_defaults), color = color) } } @@ -256,14 +256,14 @@ fun IntSettingRow(title: String, selection: MutableState, values: List "${selection.value} $label", maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) Spacer(Modifier.size(4.dp)) Icon( if (!expanded.value) painterResource(R.drawable.ic_expand_more) else painterResource(R.drawable.ic_expand_less), generalGetString(R.string.invite_to_group_button), modifier = Modifier.padding(start = 8.dp), - tint = HighOrLowlight + tint = MaterialTheme.colors.secondary ) } DefaultExposedDropdownMenu( @@ -317,14 +317,14 @@ fun TimeoutSettingRow(title: String, selection: MutableState, values: List "${df.format(selection.value / 1_000_000.toDouble())} $label", maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) Spacer(Modifier.size(4.dp)) Icon( if (!expanded.value) painterResource(R.drawable.ic_expand_more) else painterResource(R.drawable.ic_expand_less), generalGetString(R.string.invite_to_group_button), modifier = Modifier.padding(start = 8.dp), - tint = HighOrLowlight + tint = MaterialTheme.colors.secondary ) } DefaultExposedDropdownMenu( @@ -376,11 +376,11 @@ fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boo Icon( icon, title, - tint = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) Text( title, - color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary + color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } } 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 ae84518bfd..1f04dcd4f4 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,16 +1,23 @@ package chat.simplex.app.views.usersettings import SectionBottomSpacer -import SectionCustomFooter import SectionDividerSpaced +import SectionItemView import SectionItemViewSpaceBetween import SectionSpacer import SectionView import android.app.Activity import android.content.ComponentName +import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -32,13 +39,15 @@ 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.model.SharedPreference +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import com.godaddy.android.colorpicker.* import kotlinx.coroutines.delay +import kotlinx.serialization.encodeToString +import java.io.BufferedOutputStream import java.util.* +import kotlin.collections.ArrayList enum class AppIcon(val resId: Int) { DEFAULT(R.mipmap.icon), @@ -46,7 +55,7 @@ enum class AppIcon(val resId: Int) { } @Composable -fun AppearanceView(m: ChatModel) { +fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } fun setAppIcon(newIcon: AppIcon) { @@ -69,10 +78,12 @@ fun AppearanceView(m: ChatModel) { AppearanceLayout( appIcon, m.controller.appPrefs.appLanguage, + m.controller.appPrefs.systemDarkTheme, changeIcon = ::setAppIcon, - editPrimaryColor = { primary -> + showSettingsModal = showSettingsModal, + editColor = { name, initialColor -> ModalManager.shared.showModalCloseable { close -> - ColorEditor(primary, close) + ColorEditor(name, initialColor, close) } }, ) @@ -81,8 +92,10 @@ fun AppearanceView(m: ChatModel) { @Composable fun AppearanceLayout( icon: MutableState, languagePref: SharedPreference, + systemDarkTheme: SharedPreference, changeIcon: (AppIcon) -> Unit, - editPrimaryColor: (Color) -> Unit, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + editColor: (ThemeColor, Color) -> Unit, ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), @@ -127,7 +140,7 @@ fun AppearanceView(m: ChatModel) { contentDescription = "", contentScale = ContentScale.Fit, modifier = Modifier - .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondary) + .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant) .size(70.dp) .clickable { changeIcon(item) } .padding(10.dp) @@ -144,26 +157,110 @@ fun AppearanceView(m: ChatModel) { val currentTheme by CurrentColors.collectAsState() SectionView(stringResource(R.string.settings_section_title_themes)) { val darkTheme = isSystemInDarkTheme() - val state = remember { derivedStateOf { currentTheme.second } } + val state = remember { derivedStateOf { currentTheme.name } } ThemeSelector(state) { - ThemeManager.applyTheme(it.name, darkTheme) + ThemeManager.applyTheme(it, darkTheme) } - SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) { + if (state.value == DefaultTheme.SYSTEM.name) { + DarkThemeSelector(remember { systemDarkTheme.state }) { + ThemeManager.changeDarkTheme(it, darkTheme) + } + } + } + SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(R.string.customize_theme_title)) } + SectionBottomSpacer() + } +} + +@Composable +fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + val currentTheme by CurrentColors.collectAsState() + + AppBarTitle(stringResource(R.string.customize_theme_title)) + + SectionView(stringResource(R.string.theme_colors_section_title)) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { val title = generalGetString(R.string.color_primary) Text(title) Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.primary) } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { + val title = generalGetString(R.string.color_primary_variant) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.primaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { + val title = generalGetString(R.string.color_secondary) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.secondary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { + val title = generalGetString(R.string.color_secondary_variant) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.secondaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { + val title = generalGetString(R.string.color_background) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.background) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { + val title = generalGetString(R.string.color_surface) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = colors.surface) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { + val title = generalGetString(R.string.color_title) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = currentTheme.appColors.title) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { + val title = generalGetString(R.string.color_sent_message) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { + val title = generalGetString(R.string.color_received_message) + Text(title) + Icon(painterResource(R.drawable.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) + } } - 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)) + val isInDarkTheme = isInDarkTheme() + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) { + SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { + Text(generalGetString(R.string.reset_color), color = colors.primary) + } + } + SectionSpacer() + SectionView { + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) { + val context = LocalContext.current + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberSaveThemeLauncher(context, theme) + SectionItemView({ + val overrides = ThemeManager.currentThemeOverrides(isInDarkTheme) + theme.value = yaml.encodeToString(overrides) + exportThemeLauncher.launch("simplex.theme") + }) { + Text(generalGetString(R.string.export_theme), color = colors.primary) } } + + val importThemeLauncher = rememberGetContentLauncher { uri: Uri? -> + if (uri != null) { + val theme = getThemeFromUri(uri) + if (theme != null) { + ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + } + } + } + // Can not limit to YAML mime type since it's unsupported by Android + SectionItemView({ importThemeLauncher.launch("*/*") }) { + Text(generalGetString(R.string.import_theme), color = colors.primary) + } } SectionBottomSpacer() } @@ -171,6 +268,7 @@ fun AppearanceView(m: ChatModel) { @Composable fun ColorEditor( + name: ThemeColor, initialColor: Color, close: () -> Unit, ) { @@ -178,17 +276,17 @@ fun ColorEditor( Modifier .fillMaxWidth() ) { - AppBarTitle(stringResource(R.string.color_primary)) + AppBarTitle(name.text) var currentColor by remember { mutableStateOf(initialColor) } ColorPicker(initialColor) { currentColor = it } SectionSpacer() - + val isInDarkTheme = isInDarkTheme() TextButton( onClick = { - ThemeManager.saveAndApplyPrimaryColor(currentColor) + ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) close() }, Modifier.align(Alignment.CenterHorizontally), @@ -206,7 +304,7 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { modifier = Modifier .fillMaxWidth() .height(300.dp), - showAlphaBar = false, + showAlphaBar = true, onColorChanged = { color: HsvColor -> onColorChanged(color.toColor()) } @@ -241,9 +339,9 @@ private fun LangSelector(state: State, onSelected: (String) -> Unit) { } @Composable -private fun ThemeSelector(state: State, onSelected: (DefaultTheme) -> Unit) { +private fun ThemeSelector(state: State, onSelected: (String) -> Unit) { val darkTheme = isSystemInDarkTheme() - val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) } + val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) } ExposedDropDownSettingRow( generalGetString(R.string.theme), values, @@ -254,10 +352,55 @@ private fun ThemeSelector(state: State, onSelected: (DefaultTheme) ) } +@Composable +private fun DarkThemeSelector(state: State, onSelected: (String) -> Unit) { + val values by remember { + val darkThemes = ArrayList>() + darkThemes.add(DefaultTheme.DARK.name to generalGetString(R.string.theme_dark)) + darkThemes.add(DefaultTheme.SIMPLEX.name to generalGetString(R.string.theme_simplex)) + mutableStateOf(darkThemes.toList()) + } + ExposedDropDownSettingRow( + generalGetString(R.string.dark_theme), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { if (it != null) onSelected(it) } + ) +} + //private fun openSystemLangPicker(activity: Activity) { // activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName))) //} +@Composable +private fun rememberSaveThemeLauncher(cxt: Context, theme: MutableState): ManagedActivityResultLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument(), + onResult = { destination -> + try { + destination?.let { + val theme = theme.value + if (theme != null) { + val contentResolver = cxt.contentResolver + contentResolver.openOutputStream(destination)?.let { stream -> + BufferedOutputStream(stream).use { outputStream -> + theme.byteInputStream().use { it.copyTo(outputStream) } + } + Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show() + } + } + } + } catch (e: Error) { + Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show() + Log.e(TAG, "rememberSaveThemeLauncher error saving theme $e") + } finally { + theme.value = null + } + } + ) + private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> SimplexApp.context.packageManager.getComponentEnabledSetting( ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") @@ -271,8 +414,10 @@ fun PreviewAppearanceSettings() { AppearanceLayout( icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, languagePref = SharedPreference({ null }, {}), + systemDarkTheme = SharedPreference({ null }, {}), changeIcon = {}, - editPrimaryColor = {}, + showSettingsModal = { {} }, + editColor = { _, _ -> }, ) } } 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 e487276231..997a9f9e54 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 @@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.helpers.* @Composable @@ -108,7 +107,7 @@ fun SharedPreferenceToggleWithIcon( ) { val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) } Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text(text, Modifier.padding(end = 4.dp), color = if (stopped) HighOrLowlight else Color.Unspecified) + Text(text, Modifier.padding(end = 4.dp), color = if (stopped) MaterialTheme.colors.secondary else Color.Unspecified) Icon( icon, null, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt index 75017d8478..676cbd3600 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt @@ -77,7 +77,7 @@ private fun HiddenProfileLayout( PassphraseField(confirmHidePassword, stringResource(R.string.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword) } SectionItemViewSpaceBetween({ saveProfilePassword(hidePassword.value) }, disabled = saveDisabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(R.string.save_profile_password), color = if (saveDisabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(generalGetString(R.string.save_profile_password), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password)) 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 dcaad615cf..c20e4eb0be 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 @@ -207,7 +207,7 @@ fun UseSocksProxySwitch( Icon( painterResource(R.drawable.ic_settings_ethernet), stringResource(R.string.network_socks_toggle), - tint = HighOrLowlight + tint = MaterialTheme.colors.secondary ) TextIconSpaced(false) if (networkUseSocksProxy.value) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt index 6d3190ab53..e80283ecba 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt @@ -88,7 +88,7 @@ fun NotificationsSettingsLayout( modes.first { it.value == notificationsMode.value }.title, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } SettingsActionItemWithContent(null, stringResource(R.string.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { @@ -96,7 +96,7 @@ fun NotificationsSettingsLayout( previewModes.first { it.value == notificationPreviewMode.value }.title, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt index a8fe451c5e..ad0537331b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt @@ -112,7 +112,7 @@ private fun TimedMessagesFeatureSection(allowFeature: State, onS PreferenceToggleWithIcon( ChatFeature.TimedMessages.text, ChatFeature.TimedMessages.icon, - HighOrLowlight, + MaterialTheme.colors.secondary, allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES, extraPadding = false, onSelected @@ -125,10 +125,10 @@ private fun TimedMessagesFeatureSection(allowFeature: State, onS private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { SectionView { SectionItemView(reset, disabled = disabled) { - Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } SectionItemView(save, disabled = disabled) { - Text(stringResource(R.string.save_and_notify_contacts), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + Text(stringResource(R.string.save_and_notify_contacts), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt index 9611e6d900..dc960dfd8b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt @@ -60,7 +60,7 @@ fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerPr Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } @@ -107,7 +107,7 @@ private fun PresetServer( Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), style = TextStyle( fontFamily = FontFamily.Monospace, fontSize = 16.sp, - color = HighOrLowlight + color = MaterialTheme.colors.secondary ) ) } @@ -172,13 +172,13 @@ private fun UseServerSection( ) { SectionView(stringResource(R.string.smp_servers_use_server).uppercase()) { SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) { - Text(stringResource(R.string.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else HighOrLowlight) + Text(stringResource(R.string.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) ShowTestStatus(server) } val enabled = rememberUpdatedState(server.enabled) PreferenceToggle(stringResource(R.string.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) } SectionItemView(onDelete, disabled = testing) { - Text(stringResource(R.string.smp_servers_delete_server), color = if (testing) HighOrLowlight else MaterialTheme.colors.error) + Text(stringResource(R.string.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt index 173f25c5c0..5e07e6fdba 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt @@ -166,7 +166,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () Modifier .padding(horizontal = 2.dp) .size(30.dp), - color = HighOrLowlight, + color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp ) } @@ -207,8 +207,8 @@ private fun ProtocolServersLayout( stringResource(R.string.smp_servers_add), addServer, disabled = testing, - textColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary, - iconColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary + textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } SectionTextFooter( @@ -225,14 +225,14 @@ private fun ProtocolServersLayout( SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { SectionItemView(resetServers, disabled = serversUnchanged) { - Text(stringResource(R.string.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else HighOrLowlight) + Text(stringResource(R.string.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) } val testServersDisabled = testing || allServersDisabled SectionItemView(testServers, disabled = testServersDisabled) { - Text(stringResource(R.string.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else HighOrLowlight) + Text(stringResource(R.string.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) } SectionItemView(saveSMPServers, disabled = saveDisabled) { - Text(stringResource(R.string.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else HighOrLowlight) + Text(stringResource(R.string.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) } } SectionDividerSpaced(maxBottomPadding = false) @@ -248,15 +248,15 @@ private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, s val address = parseServerAddress(srv.server) when { address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() - !srv.enabled -> Icon(painterResource(R.drawable.ic_do_not_disturb_on), null, tint = HighOrLowlight) + !srv.enabled -> Icon(painterResource(R.drawable.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) else -> ShowTestStatus(srv) } Spacer(Modifier.padding(horizontal = 4.dp)) val text = address?.hostnames?.firstOrNull() ?: srv.server if (srv.enabled) { - Text(text, color = if (disabled) HighOrLowlight else MaterialTheme.colors.onBackground, maxLines = 1) + Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) } else { - Text(text, maxLines = 1, color = HighOrLowlight) + Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt index fde4082739..bfecd10027 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt @@ -155,7 +155,7 @@ fun RTCServersLayout( .height(160.dp) .fillMaxWidth(), shape = RoundedCornerShape(10.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.secondary) + border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant) ) { SelectionContainer( Modifier.verticalScroll(rememberScrollState()) 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 c28aa38f5a..2c90600077 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 @@ -97,7 +97,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) } } Box( - Modifier.fillMaxSize(), + Modifier.fillMaxSize().background(MaterialTheme.colors.background), contentAlignment = Alignment.Center ) { SimpleButton( @@ -134,8 +134,9 @@ fun SettingsLayout( showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { + val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).background(MaterialTheme.colors.background)) { + Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).themedBackground(theme.value.base)) { Column( Modifier .fillMaxSize() @@ -160,7 +161,7 @@ fun SettingsLayout( SettingsActionItem(painterResource(R.drawable.ic_wifi_tethering), stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(R.drawable.ic_videocam), stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(R.drawable.ic_lock), stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(R.drawable.ic_light_mode), stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(R.drawable.ic_light_mode), stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } SectionDividerSpaced() @@ -199,7 +200,7 @@ fun SettingsIncognitoActionItem( ) { SettingsPreferenceItemWithInfo( if (incognito.value) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_theater_comedy), - if (incognito.value) Indigo else HighOrLowlight, + if (incognito.value) Indigo else MaterialTheme.colors.secondary, stringResource(R.string.incognito), stopped, onClickInfo, @@ -237,7 +238,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { Icon( painterResource(R.drawable.ic_database), contentDescription = stringResource(R.string.database_passphrase_and_export), - tint = if (encrypted) HighOrLowlight else WarningOrange, + tint = if (encrypted) MaterialTheme.colors.secondary else WarningOrange, ) TextIconSpaced(true) Text(stringResource(R.string.database_passphrase_and_export)) @@ -282,10 +283,10 @@ fun ChatLockItem( click = showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }, icon = if (performLA.value) painterResource(R.drawable.ic_lock_filled) else painterResource(R.drawable.ic_lock), text = stringResource(R.string.chat_lock), - iconColor = if (performLA.value) SimplexGreen else HighOrLowlight, + iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary, extraPadding = false, ) { - Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight) + Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = MaterialTheme.colors.secondary) } } @@ -294,7 +295,7 @@ fun ChatLockItem( Icon( painterResource(R.drawable.ic_keyboard), contentDescription = "GitHub", - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) TextIconSpaced(extraPadding = true) Text(generalGetString(R.string.contribute), color = MaterialTheme.colors.primary) @@ -310,7 +311,7 @@ fun ChatLockItem( Icon( painterResource(R.drawable.ic_star), contentDescription = "Google Play", - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) TextIconSpaced(extraPadding = true) Text(generalGetString(R.string.rate_the_app), color = MaterialTheme.colors.primary) @@ -322,7 +323,7 @@ fun ChatLockItem( Icon( painter = painterResource(id = R.drawable.ic_github), contentDescription = "GitHub", - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) TextIconSpaced(extraPadding = true) Text(generalGetString(R.string.star_on_github), color = MaterialTheme.colors.primary) @@ -334,7 +335,7 @@ fun ChatLockItem( Icon( painter = painterResource(id = R.drawable.ic_outline_terminal), contentDescription = stringResource(R.string.chat_console), - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) TextIconSpaced() Text(stringResource(R.string.chat_console)) @@ -346,7 +347,7 @@ fun ChatLockItem( Icon( painter = painterResource(id = R.drawable.ic_github), contentDescription = "GitHub", - tint = HighOrLowlight, + tint = MaterialTheme.colors.secondary, ) TextIconSpaced() Text(generalGetString(R.string.install_simplex_chat_for_terminal), color = MaterialTheme.colors.primary) @@ -361,7 +362,7 @@ fun ChatLockItem( Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } -@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) { +@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondaryVariant, stopped: Boolean = false) { ProfileImage(size = size, image = profileOf.image, color = color) Spacer(Modifier.padding(horizontal = 8.dp)) Column(Modifier.height(size), verticalArrangement = Arrangement.Center) { @@ -369,7 +370,7 @@ fun ChatLockItem( profileOf.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, - color = if (stopped) HighOrLowlight else Color.Unspecified, + color = if (stopped) MaterialTheme.colors.secondary else Color.Unspecified, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -377,7 +378,7 @@ fun ChatLockItem( Text( profileOf.fullName, Modifier.padding(vertical = 5.dp), - color = if (stopped) HighOrLowlight else Color.Unspecified, + color = if (stopped) MaterialTheme.colors.secondary else Color.Unspecified, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -386,16 +387,16 @@ fun ChatLockItem( } @Composable -fun SettingsActionItem(icon: Painter, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, iconColor: Color = HighOrLowlight, disabled: Boolean = false, extraPadding: Boolean = false) { +fun SettingsActionItem(icon: Painter, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false) { SectionItemView(click, disabled = disabled, extraPadding = extraPadding) { - Icon(icon, text, tint = if (disabled) HighOrLowlight else iconColor) + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor) TextIconSpaced(extraPadding) - Text(text, color = if (disabled) HighOrLowlight else textColor) + Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor) } } @Composable -fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = HighOrLowlight, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) { +fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) { SectionItemView( click, extraPadding = extraPadding, @@ -406,11 +407,11 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: ( disabled = disabled ) { if (icon != null) { - Icon(icon, text, Modifier, tint = if (disabled) HighOrLowlight else iconColor) + Icon(icon, text, Modifier, tint = if (disabled) MaterialTheme.colors.secondary else iconColor) TextIconSpaced(extraPadding) } if (text != null) { - Text(text, Modifier.weight(1f), color = if (disabled) HighOrLowlight else MaterialTheme.colors.onBackground) + Text(text, Modifier.weight(1f), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING)) } content() @@ -422,7 +423,7 @@ fun SettingsPreferenceItem( icon: Painter?, text: String, pref: SharedPreference, - iconColor: Color = HighOrLowlight, + iconColor: Color = MaterialTheme.colors.secondary, enabled: Boolean = true, onChange: ((Boolean) -> Unit)? = null, ) { @@ -464,12 +465,12 @@ fun PreferenceToggle( fun PreferenceToggleWithIcon( text: String, icon: Painter? = null, - iconColor: Color? = HighOrLowlight, + iconColor: Color? = MaterialTheme.colors.secondary, checked: Boolean, extraPadding: Boolean = false, onChange: (Boolean) -> Unit = {}, ) { - SettingsActionItemWithContent(icon, text, iconColor = iconColor ?: HighOrLowlight, extraPadding = extraPadding) { + SettingsActionItemWithContent(icon, text, iconColor = iconColor ?: MaterialTheme.colors.secondary, extraPadding = extraPadding) { DefaultSwitch( checked = checked, onCheckedChange = { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt index 5e70e268db..e0d7926486 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -115,7 +115,7 @@ fun UserProfileLayout( ) { Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f)) + ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) EditImageButton { scope.launch { bottomSheetModalState.show() } } } if (profileImage.value != null) { @@ -156,7 +156,7 @@ fun UserProfileLayout( saveColor = MaterialTheme.colors.primary } else { saveModifier = Modifier - saveColor = HighOrLowlight + saveColor = MaterialTheme.colors.secondary } Text( stringResource(R.string.save_and_notify_contacts), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt index 780d9292f9..98c1519320 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt @@ -281,7 +281,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( PassphraseField(actionPassword, generalGetString(R.string.profile_password), isValid = { passwordValid }, showStrength = true) } SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(label), color = if (actionEnabled) color else HighOrLowlight) + Text(generalGetString(label), color = if (actionEnabled) color else MaterialTheme.colors.secondary) } } } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 578c14a298..ea69b48417 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -564,6 +564,8 @@ Update transport isolation mode? Set Use .onion hosts to No if SOCKS proxy does not support them. Appearance + Customize theme + THEME COLORS App version App version: v%s App build: %s @@ -1156,15 +1158,29 @@ System Light Dark + SimpleX System Theme + Dark theme Save color + Import theme + Import theme error + Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure. + Export theme Reset colors Accent + Additional accent + Secondary + Additional secondary + Background + Menus & alerts + Title + Sent message + Received message You allow From 607f77d432bb1a2d048038cb65462cf6897ae4be Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:09:42 +0300 Subject: [PATCH 08/22] android: more checks for colors in customized theme (#2343) * android: more checks for colors in customized theme * code style * refactor --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/ui/theme/Theme.kt | 21 ++++++++++++------- .../app/views/usersettings/Appearance.kt | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) 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 1f1b4dcdeb..85b3a0dbdf 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 @@ -21,21 +21,26 @@ enum class DefaultTheme { SYSTEM, LIGHT, DARK, SIMPLEX; // Call it only with base theme, not SYSTEM - fun hasChangedAnyColor(colors: Colors): Boolean { + fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean { val palette = when (this) { SYSTEM -> return false LIGHT -> LightColorPalette DARK -> DarkColorPalette SIMPLEX -> SimplexColorPalette } - return with(palette) { - colors.primary != primary || - colors.primaryVariant != primaryVariant || - colors.secondary != secondary || - colors.secondaryVariant != secondaryVariant || - colors.background != background || - colors.surface != surface + val appPalette = when (this) { + SYSTEM -> return false + LIGHT -> LightColorPaletteApp + DARK -> DarkColorPaletteApp + SIMPLEX -> SimplexColorPaletteApp } + return colors.primary != palette.primary || + colors.primaryVariant != palette.primaryVariant || + colors.secondary != palette.secondary || + colors.secondaryVariant != palette.secondaryVariant || + colors.background != palette.background || + colors.surface != palette.surface || + appColors != appPalette } } 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 1f04dcd4f4..4b5e65d710 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 @@ -229,14 +229,14 @@ fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { } } val isInDarkTheme = isInDarkTheme() - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) { + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { Text(generalGetString(R.string.reset_color), color = colors.primary) } } SectionSpacer() SectionView { - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors)) { + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { val context = LocalContext.current val theme = remember { mutableStateOf(null as String?) } val exportThemeLauncher = rememberSaveThemeLauncher(context, theme) From 2a883bb9582a097d1c0293a4b3b98b086c0083f8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 28 Apr 2023 12:14:07 +0400 Subject: [PATCH 09/22] core: add /profile_address command to help (#2345) --- src/Simplex/Chat/Help.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index 37770d1bd1..1f77617af1 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -201,6 +201,7 @@ myAddressHelpInfo = styleMarkdown [ green "Your contact address commands:", indent <> highlight "/address " <> " - create your address", + indent <> highlight "/profile_address on/off " <> " - share address with your contacts (it will be added to your profile)", indent <> highlight "/delete_address " <> " - delete your address (accepted contacts will remain connected)", indent <> highlight "/show_address " <> " - show your address", indent <> highlight "/accept " <> " - accept contact request", From 90cee6b8026aaa78f49eaf3b589732c2070a1199 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 28 Apr 2023 12:21:23 +0400 Subject: [PATCH 10/22] ios: scroll address view on keyboard, translations (#2344) * ios: scroll address view on keyboard, translations * fix typo * revert text --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../UserSettings/UserAddressLearnMore.swift | 6 +- .../Views/UserSettings/UserAddressView.swift | 56 ++++--- .../cs.xcloc/Localized Contents/cs.xliff | 114 +++++++++++--- .../de.xcloc/Localized Contents/de.xliff | 112 +++++++++++--- .../en.xcloc/Localized Contents/en.xliff | 140 ++++++++++++++---- .../es.xcloc/Localized Contents/es.xliff | 114 +++++++++++--- .../fr.xcloc/Localized Contents/fr.xliff | 114 +++++++++++--- .../it.xcloc/Localized Contents/it.xliff | 114 +++++++++++--- .../nl.xcloc/Localized Contents/nl.xliff | 114 +++++++++++--- .../pl.xcloc/Localized Contents/pl.xliff | 114 +++++++++++--- .../ru.xcloc/Localized Contents/ru.xliff | 112 +++++++++++--- .../Localized Contents/zh-Hans.xliff | 112 +++++++++++--- 12 files changed, 971 insertions(+), 251 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index df7bcf2767..e944dcec95 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -12,9 +12,9 @@ struct UserAddressLearnMore: View { var body: some View { List { VStack(alignment: .leading, spacing: 18) { - Text("You can create a long term address that can be used by other people to connect with you.") - Text("Unlike 1-time invitation links, these addresses can be used many times, that makes them good to share online.") - Text("When people connect to you via this address, you will receive a connection request that you can accept or reject.") + Text("You can share your address as a link or QR code - anybody can connect to you.") + Text("You won't lose your contacts if you later delete your address.") + Text("When people request to connect, you can accept or reject it.") Text("Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address).") } .listRowBackground(Color.clear) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 1dbd466f50..802cf4d4a5 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -41,13 +41,14 @@ struct UserAddressView: View { var body: some View { ZStack { if viaCreateLinkView { - userAddressView() + userAddressScrollView() } else { - userAddressView() + userAddressScrollView() .modifier(BackButton { if savedAAS == aas { dismiss() } else { + keyboardVisible = false showSaveDialogue = true } }) @@ -73,7 +74,24 @@ struct UserAddressView: View { } } - @ViewBuilder private func userAddressView() -> some View { + @Namespace private var bottomID + + private func userAddressScrollView() -> some View { + ScrollViewReader { proxy in + userAddressView() + .onChange(of: keyboardVisible) { _ in + if keyboardVisible { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + proxy.scrollTo(bottomID, anchor: .top) + } + } + } + } + } + } + + private func userAddressView() -> some View { List { if let userAddress = chatModel.userAddress { existingAddressView(userAddress) @@ -187,6 +205,7 @@ struct UserAddressView: View { } footer: { Text("Your contacts will remain connected.") } + .id(bottomID) } private func createAddressButton() -> some View { @@ -247,7 +266,7 @@ struct UserAddressView: View { .navigationBarTitleDisplayMode(.large) } label: { settingsRow("info.circle") { - Text("Learn more") + Text("About SimpleX address") } } } @@ -327,7 +346,7 @@ struct UserAddressView: View { saveAASButton() .disabled(aas == savedAAS) } header: { - Text("Accept requests") + Text("Auto-accept") } } @@ -342,26 +361,25 @@ struct UserAddressView: View { private func welcomeMessageEditor() -> some View { ZStack { - if aas.welcomeText.isEmpty { - TextEditor(text: Binding.constant("Enter welcome message… (optional)")) - .foregroundColor(.secondary) - .disabled(true) - .padding(.horizontal, -5) - .padding(.top, -8) - .frame(height: 90, alignment: .topLeading) - .frame(maxWidth: .infinity, alignment: .leading) + Group { + if aas.welcomeText.isEmpty { + TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"))) + .foregroundColor(.secondary) + .disabled(true) + } + TextEditor(text: $aas.welcomeText) + .focused($keyboardVisible) } - TextEditor(text: $aas.welcomeText) - .focused($keyboardVisible) - .padding(.horizontal, -5) - .padding(.top, -8) - .frame(height: 90, alignment: .topLeading) - .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 90, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) } } private func saveAASButton() -> some View { Button { + keyboardVisible = false saveAAS() } label: { Text("Save") diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 775e7c754d..0980517f78 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -361,6 +361,10 @@ Dostupné ve verzi 5.1 O SimpleX chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Zbarvení @@ -387,9 +391,8 @@ Dostupné ve verzi 5.1 Přijmout inkognito No comment provided by engineer. - - Accept requests - Přijímat žádosti + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Dostupné ve verzi 5.1 Přidat uvítací zprávu No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Správci mohou vytvářet odkazy pro připojení ke skupinám. @@ -447,9 +454,12 @@ Dostupné ve verzi 5.1 Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás. No comment provided by engineer. - - All your contacts will remain connected - Všechny vaše kontakty zůstanou připojeny + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Dostupné ve verzi 5.1 Ověřování není k dispozici No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Automatické přijímání žádostí o kontakt @@ -622,11 +636,6 @@ Dostupné ve verzi 5.1 Automaticky přijímat obrázky No comment provided by engineer. - - Automatically - Automaticky - No comment provided by engineer. - Back Zpět @@ -992,11 +1001,6 @@ Dostupné ve verzi 5.1 Předvolby kontaktů No comment provided by engineer. - - Contact requests - Žádosti o kontakt - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit. @@ -1017,9 +1021,12 @@ Dostupné ve verzi 5.1 Vytvořit No comment provided by engineer. - - Create address - Vytvořit adresu + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Dostupné ve verzi 5.1 Zadejte server ručně No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Chyba @@ -2343,6 +2354,10 @@ Dostupné ve verzi 5.1 Velký soubor! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Opustit @@ -2987,6 +3002,10 @@ Dostupné ve verzi 5.1 Heslo profilu No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Zákaz audio/video hovorů. @@ -3247,6 +3266,10 @@ Dostupné ve verzi 5.1 Uložit archiv No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Uložení profilu skupiny @@ -3282,6 +3305,10 @@ Dostupné ve verzi 5.1 Uložit servery? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Uložit uvítací zprávu? @@ -3472,6 +3499,14 @@ Dostupné ve verzi 5.1 Sdílet chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Sdílet odkaz na pozvánku @@ -3487,6 +3522,10 @@ Dostupné ve verzi 5.1 Jednorázový zvací odkaz No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Zobrazit QR kód @@ -3537,6 +3576,10 @@ Dostupné ve verzi 5.1 Zapnutý zámek SimpleX Lock No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX kontaktní adresa @@ -3632,6 +3675,14 @@ Dostupné ve verzi 5.1 Zastavit odesílání souboru? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Odeslat @@ -4228,6 +4279,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Když je k dispozici No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve. @@ -4320,9 +4375,13 @@ SimpleX zámek musí být povolen. Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. O své kontakty nepřijdete, pokud ji později smažete. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX zámek musí být povolen. Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + O své kontakty nepřijdete, pokud ji později smažete. + 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 Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil @@ -4462,7 +4526,7 @@ SimpleX zámek musí být povolen. Your SimpleX contact address - Vaše kontaktní adresa SimpleX + Vaše adresa SimpleX No comment provided by engineer. @@ -4532,6 +4596,10 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov Vaše kontakty mohou povolit úplné mazání zpráv. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 14b40892cf..68ccbbb7a3 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -361,6 +361,10 @@ Verfügbar ab v5.1 Über SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Akzentfarbe @@ -387,9 +391,8 @@ Verfügbar ab v5.1 Inkognito akzeptieren No comment provided by engineer. - - Accept requests - Anfragen annehmen + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Verfügbar ab v5.1 Begrüßungsmeldung hinzufügen No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Administratoren können Links für den Beitritt zu Gruppen erzeugen. @@ -447,9 +454,12 @@ Verfügbar ab v5.1 Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. No comment provided by engineer. - - All your contacts will remain connected - Alle Ihre Kontakte bleiben verbunden + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Verfügbar ab v5.1 Authentifizierung nicht verfügbar No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Kontaktanfragen automatisch annehmen @@ -622,11 +636,6 @@ Verfügbar ab v5.1 Bilder automatisch akzeptieren No comment provided by engineer. - - Automatically - Automatisch - No comment provided by engineer. - Back Zurück @@ -992,11 +1001,6 @@ Verfügbar ab v5.1 Kontakt Präferenzen No comment provided by engineer. - - Contact requests - Kontaktanfragen - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen. @@ -1017,9 +1021,12 @@ Verfügbar ab v5.1 Erstellen No comment provided by engineer. - - Create address - Adresse erstellen + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Verfügbar ab v5.1 Geben Sie den Server manuell ein No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Fehler @@ -2343,6 +2354,10 @@ Verfügbar ab v5.1 Große Datei! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Verlassen @@ -2987,6 +3002,10 @@ Verfügbar ab v5.1 Passwort für Profil No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Audio/Video Anrufe nicht erlauben. @@ -3247,6 +3266,10 @@ Verfügbar ab v5.1 Archiv speichern No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Gruppenprofil speichern @@ -3282,6 +3305,10 @@ Verfügbar ab v5.1 Alle Server speichern? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Begrüßungsmeldung speichern? @@ -3472,6 +3499,14 @@ Verfügbar ab v5.1 Teilen chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Einladungslink teilen @@ -3487,6 +3522,10 @@ Verfügbar ab v5.1 Einmal-Einladungslink teilen No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code QR-Code anzeigen @@ -3537,6 +3576,10 @@ Verfügbar ab v5.1 SimpleX Sperre aktiviert No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX Kontaktadressen-Link @@ -3632,6 +3675,14 @@ Verfügbar ab v5.1 Das Senden der Datei beenden? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Übermitteln @@ -4228,6 +4279,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wenn verfügbar No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden. @@ -4320,9 +4375,13 @@ Dafür muss die SimpleX Sperre aktiviert sein. Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. No comment provided by engineer. @@ -4435,6 +4494,11 @@ Dafür muss die SimpleX Sperre aktiviert sein. Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen. + 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 Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden @@ -4532,6 +4596,10 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT. 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 c0a0c7cfc1..d7c662591e 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -361,6 +361,11 @@ Available in v5.1 About SimpleX Chat No comment provided by engineer. + + About SimpleX address + About SimpleX address + No comment provided by engineer. + Accent color Accent color @@ -387,9 +392,9 @@ Available in v5.1 Accept incognito No comment provided by engineer. - - Accept requests - Accept requests + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +427,11 @@ Available in v5.1 Add welcome message No comment provided by engineer. + + Address + Address + No comment provided by engineer. + Admins can create the links to join groups. Admins can create the links to join groups. @@ -447,9 +457,14 @@ Available in v5.1 All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. No comment provided by engineer. - - All your contacts will remain connected - All your contacts will remain connected + + All your contacts will remain connected. + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +627,11 @@ Available in v5.1 Authentication unavailable No comment provided by engineer. + + Auto-accept + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Auto-accept contact requests @@ -622,11 +642,6 @@ Available in v5.1 Auto-accept images No comment provided by engineer. - - Automatically - Automatically - No comment provided by engineer. - Back Back @@ -992,11 +1007,6 @@ Available in v5.1 Contact preferences No comment provided by engineer. - - Contact requests - Contact requests - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Contacts can mark messages for deletion; you will be able to view them. @@ -1017,9 +1027,14 @@ Available in v5.1 Create No comment provided by engineer. - - Create address - Create address + + Create SimpleX address + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1605,11 @@ Available in v5.1 Enter server manually No comment provided by engineer. + + Enter welcome message… (optional) + Enter welcome message… (optional) + placeholder + Error Error @@ -2343,6 +2363,11 @@ Available in v5.1 Large file! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Leave @@ -2987,6 +3012,11 @@ Available in v5.1 Profile password No comment provided by engineer. + + Profile update will be sent to your contacts. + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Prohibit audio/video calls. @@ -3247,6 +3277,11 @@ Available in v5.1 Save archive No comment provided by engineer. + + Save auto-accept settings + Save auto-accept settings + No comment provided by engineer. + Save group profile Save group profile @@ -3282,6 +3317,11 @@ Available in v5.1 Save servers? No comment provided by engineer. + + Save settings? + Save settings? + No comment provided by engineer. + Save welcome message? Save welcome message? @@ -3472,6 +3512,16 @@ Available in v5.1 Share chat item action + + Share address + Share address + No comment provided by engineer. + + + Share address with contacts? + Share address with contacts? + No comment provided by engineer. + Share invitation link Share invitation link @@ -3487,6 +3537,11 @@ Available in v5.1 Share one-time invitation link No comment provided by engineer. + + Share with contacts + Share with contacts + No comment provided by engineer. + Show QR code Show QR code @@ -3537,6 +3592,11 @@ Available in v5.1 SimpleX Lock turned on No comment provided by engineer. + + SimpleX address + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX contact address @@ -3632,6 +3692,16 @@ Available in v5.1 Stop sending file? No comment provided by engineer. + + Stop sharing + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + Stop sharing address? + No comment provided by engineer. + Submit Submit @@ -4228,6 +4298,11 @@ To connect, please ask your contact to create another connection link and check When available No comment provided by engineer. + + When people request to connect, you can accept or reject it. + When people request to connect, you can accept or reject it. + No comment provided by engineer. + 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. @@ -4320,9 +4395,14 @@ SimpleX Lock must be enabled. You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. + + You can share this address with your contacts to let them connect with **%@**. + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + You can share your address as a link or QR code - anybody can connect to you. No comment provided by engineer. @@ -4435,6 +4515,11 @@ SimpleX Lock must be enabled. You will stop receiving messages from this group. Chat history will be preserved. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + You won't lose your contacts if you later delete your address. + 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 @@ -4460,9 +4545,9 @@ SimpleX Lock must be enabled. Your SMP servers No comment provided by engineer. - - Your SimpleX contact address - Your SimpleX contact address + + Your SimpleX address + Your SimpleX address No comment provided by engineer. @@ -4532,6 +4617,11 @@ You can cancel this connection and remove the contact (and try later with a new Your contacts can allow full message deletion. No comment provided by engineer. + + Your contacts will remain connected. + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Your current chat database will be DELETED and REPLACED with the imported one. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index b87a02742f..148a814c74 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -361,6 +361,10 @@ Disponible en v5.1 Acerca de SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Color @@ -387,9 +391,8 @@ Disponible en v5.1 Aceptar incógnito No comment provided by engineer. - - Accept requests - Aceptar solicitudes + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Disponible en v5.1 Agregar mensaje de bienvenida No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Los administradores pueden crear los enlaces para unirse a los grupos. @@ -447,9 +454,12 @@ Disponible en v5.1 Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse! No comment provided by engineer. - - All your contacts will remain connected - Todos tus contactos permanecerán conectados + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Disponible en v5.1 Autenticación no disponible No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Aceptar automáticamente solicitudes del contacto @@ -622,11 +636,6 @@ Disponible en v5.1 Aceptar automáticamente imágenes No comment provided by engineer. - - Automatically - Automáticamente - No comment provided by engineer. - Back Volver @@ -992,11 +1001,6 @@ Disponible en v5.1 Preferencias de contacto No comment provided by engineer. - - Contact requests - Solicitud del contacto - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. El contacto solo puede marcar los mensajes para eliminar. Tu podrás verlos. @@ -1017,9 +1021,12 @@ Disponible en v5.1 Crear No comment provided by engineer. - - Create address - Crear dirección + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Disponible en v5.1 Introduce el servidor manualmente No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Error @@ -2343,6 +2354,10 @@ Disponible en v5.1 ¡Archivo grande! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Salir @@ -2987,6 +3002,10 @@ Disponible en v5.1 Contraseña del perfil No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Prohibir las llamadas/videollamadas. @@ -3247,6 +3266,10 @@ Disponible en v5.1 Guardar archivo No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Guardar perfil de grupo @@ -3282,6 +3305,10 @@ Disponible en v5.1 ¿Guardar servidores? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? ¿Guardar mensaje de bienvenida? @@ -3472,6 +3499,14 @@ Disponible en v5.1 Compartir chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Compartir enlace de invitación @@ -3487,6 +3522,10 @@ Disponible en v5.1 Compartir enlace de invitación de un uso No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Mostrar código QR @@ -3537,6 +3576,10 @@ Disponible en v5.1 Bloqueo SimpleX activado No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address Dirección de contacto SimpleX @@ -3632,6 +3675,14 @@ Disponible en v5.1 ¿Dejar de enviar el archivo? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Enviar @@ -4229,6 +4280,10 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Cuando esté disponible No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten. @@ -4321,9 +4376,13 @@ Bloqueo SimpleX debe estar activado. Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo. Si lo eliminas más tarde tus contactos no se perderán. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo. No comment provided by engineer. @@ -4436,6 +4495,11 @@ Bloqueo SimpleX debe estar activado. Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Si lo eliminas más tarde tus contactos no se perderán. + 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 Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal @@ -4463,7 +4527,7 @@ Bloqueo SimpleX debe estar activado. Your SimpleX contact address - Mi dirección de contacto SimpleX + Mi dirección SimpleX No comment provided by engineer. @@ -4533,6 +4597,10 @@ Puedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde c Tus contactos pueden permitir la eliminación completa de mensajes. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. La base de datos actual será ELIMINADA y SUSTITUIDA por la importada. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 88d5bca63d..a5c44c9ff4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -361,6 +361,10 @@ Disponible dans la v5.1 À propos de SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Couleur principale @@ -387,9 +391,8 @@ Disponible dans la v5.1 Accepter en incognito No comment provided by engineer. - - Accept requests - Accepter les demandes + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Disponible dans la v5.1 Ajouter un message d'accueil No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Les admins peuvent créer les liens qui permettent de rejoindre les groupes. @@ -447,9 +454,12 @@ Disponible dans la v5.1 Tous les messages seront supprimés - impossible de revenir en arrière ! Les messages seront supprimés UNIQUEMENT pour vous. No comment provided by engineer. - - All your contacts will remain connected - Tous vos contacts resteront connectés + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Disponible dans la v5.1 Authentification indisponible No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Demandes de contact auto-acceptées @@ -622,11 +636,6 @@ Disponible dans la v5.1 Images auto-acceptées No comment provided by engineer. - - Automatically - Automatiquement - No comment provided by engineer. - Back Retour @@ -992,11 +1001,6 @@ Disponible dans la v5.1 Préférences de contact No comment provided by engineer. - - Contact requests - Demandes de contact - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter. @@ -1017,9 +1021,12 @@ Disponible dans la v5.1 Créer No comment provided by engineer. - - Create address - Créer une adresse + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Disponible dans la v5.1 Entrer un serveur manuellement No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Erreur @@ -2343,6 +2354,10 @@ Disponible dans la v5.1 Fichier trop lourd ! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Quitter @@ -2987,6 +3002,10 @@ Disponible dans la v5.1 Mot de passe de profil No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Interdire les appels audio/vidéo. @@ -3247,6 +3266,10 @@ Disponible dans la v5.1 Sauvegarder l'archive No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Sauvegarder le profil du groupe @@ -3282,6 +3305,10 @@ Disponible dans la v5.1 Sauvegarder les serveurs ? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Sauvegarder le message d'accueil ? @@ -3472,6 +3499,14 @@ Disponible dans la v5.1 Partager chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Partager le lien d'invitation @@ -3487,6 +3522,10 @@ Disponible dans la v5.1 Partager un lien d'invitation unique No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Afficher le code QR @@ -3537,6 +3576,10 @@ Disponible dans la v5.1 SimpleX Lock activé No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address Adresse de contact SimpleX @@ -3632,6 +3675,14 @@ Disponible dans la v5.1 Arrêter l'envoi du fichier ? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Soumettre @@ -4228,6 +4279,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Quand disponible No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite. @@ -4320,9 +4375,13 @@ SimpleX Lock doit être activé. Vous pouvez partager un lien ou un code QR - n'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Vous pouvez partager votre adresse sous forme de lien ou de code QR - n'importe qui pourra se connecter à vous. Vous ne perdrez pas vos contacts si vous la supprimez par la suite. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Vous pouvez partager votre adresse sous forme de lien ou de code QR - n'importe qui pourra se connecter à vous. No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX Lock doit être activé. Vous ne recevrez plus de messages de ce groupe. L'historique du chat sera conservé. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Vous ne perdrez pas vos contacts si vous la supprimez par la suite. + 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 Vous essayez d'inviter un contact avec lequel vous avez partagé un profil incognito à rejoindre le groupe dans lequel vous utilisez votre profil principal @@ -4462,7 +4526,7 @@ SimpleX Lock doit être activé. Your SimpleX contact address - Votre adresse de contact SimpleX + Votre adresse SimpleX No comment provided by engineer. @@ -4532,6 +4596,10 @@ Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tar Vos contacts peuvent autoriser la suppression complète des messages. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 6c0ee81008..bb549d4e9f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -361,6 +361,10 @@ Disponibile nella v5.1 Riguardo SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Colore principale @@ -387,9 +391,8 @@ Disponibile nella v5.1 Accetta in incognito No comment provided by engineer. - - Accept requests - Accetta le richieste + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Disponibile nella v5.1 Aggiungi messaggio di benvenuto No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Gli amministratori possono creare i link per entrare nei gruppi. @@ -447,9 +454,12 @@ Disponibile nella v5.1 Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te. No comment provided by engineer. - - All your contacts will remain connected - Tutti i tuoi contatti resteranno connessi + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Disponibile nella v5.1 Autenticazione non disponibile No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Auto-accetta richieste di contatto @@ -622,11 +636,6 @@ Disponibile nella v5.1 Auto-accetta immagini No comment provided by engineer. - - Automatically - Automaticamente - No comment provided by engineer. - Back Indietro @@ -992,11 +1001,6 @@ Disponibile nella v5.1 Preferenze del contatto No comment provided by engineer. - - Contact requests - Richieste del contatto - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. I contatti possono contrassegnare i messaggi per l'eliminazione; potrai vederli. @@ -1017,9 +1021,12 @@ Disponibile nella v5.1 Crea No comment provided by engineer. - - Create address - Crea indirizzo + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Disponibile nella v5.1 Inserisci il server a mano No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Errore @@ -2343,6 +2354,10 @@ Disponibile nella v5.1 File grande! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Esci @@ -2987,6 +3002,10 @@ Disponibile nella v5.1 Password del profilo No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Proibisci le chiamate audio/video. @@ -3247,6 +3266,10 @@ Disponibile nella v5.1 Salva archivio No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Salva il profilo del gruppo @@ -3282,6 +3305,10 @@ Disponibile nella v5.1 Salvare i server? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Salvare il messaggio di benvenuto? @@ -3472,6 +3499,14 @@ Disponibile nella v5.1 Condividi chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Condividi link di invito @@ -3487,6 +3522,10 @@ Disponibile nella v5.1 Condividi link di invito una tantum No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Mostra codice QR @@ -3537,6 +3576,10 @@ Disponibile nella v5.1 SimpleX Lock attivato No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address Indirizzo del contatto SimpleX @@ -3632,6 +3675,14 @@ Disponibile nella v5.1 Fermare l'invio del file? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Invia @@ -4228,6 +4279,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando disponibili No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano. @@ -4320,9 +4375,13 @@ SimpleX Lock deve essere attivato. Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. Non perderai i tuoi contatti se in seguito lo elimini. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX Lock deve essere attivato. Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Non perderai i tuoi contatti se in seguito lo elimini. + 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 Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale @@ -4462,7 +4526,7 @@ SimpleX Lock deve essere attivato. Your SimpleX contact address - Il tuo indirizzo di contatto SimpleX + Il tuo indirizzo SimpleX No comment provided by engineer. @@ -4532,6 +4596,10 @@ Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tard I tuoi contatti possono consentire l'eliminazione completa dei messaggi. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Il tuo attuale database della chat verrà ELIMINATO e SOSTITUITO con quello importato. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index a144792be3..ff753124e3 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -361,6 +361,10 @@ Beschikbaar in v5.1 Over SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Accent kleur @@ -387,9 +391,8 @@ Beschikbaar in v5.1 Accepteer incognito No comment provided by engineer. - - Accept requests - Verzoeken accepteren + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Beschikbaar in v5.1 Welkomst bericht toevoegen No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Beheerders kunnen de uitnodiging links naar groepen aanmaken. @@ -447,9 +454,12 @@ Beschikbaar in v5.1 Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd. No comment provided by engineer. - - All your contacts will remain connected - Al uw contacten blijven verbonden + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Beschikbaar in v5.1 Verificatie niet beschikbaar No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Contact verzoeken automatisch accepteren @@ -622,11 +636,6 @@ Beschikbaar in v5.1 Afbeeldingen automatisch accepteren No comment provided by engineer. - - Automatically - Automatisch - No comment provided by engineer. - Back Terug @@ -992,11 +1001,6 @@ Beschikbaar in v5.1 Contact voorkeuren No comment provided by engineer. - - Contact requests - Contact verzoeken - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken. @@ -1017,9 +1021,12 @@ Beschikbaar in v5.1 Maak No comment provided by engineer. - - Create address - Adres aanmaken + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Beschikbaar in v5.1 Voer de server handmatig in No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Fout @@ -2343,6 +2354,10 @@ Beschikbaar in v5.1 Groot bestand! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Verlaten @@ -2987,6 +3002,10 @@ Beschikbaar in v5.1 Profiel wachtwoord No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Audio/video gesprekken verbieden. @@ -3247,6 +3266,10 @@ Beschikbaar in v5.1 Bewaar archief No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Groep profiel opslaan @@ -3282,6 +3305,10 @@ Beschikbaar in v5.1 Servers opslaan? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Welkomst bericht opslaan? @@ -3472,6 +3499,14 @@ Beschikbaar in v5.1 Deel chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Uitnodiging link delen @@ -3487,6 +3522,10 @@ Beschikbaar in v5.1 Eenmalige uitnodiging link delen No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Toon QR-code @@ -3537,6 +3576,10 @@ Beschikbaar in v5.1 SimpleX Vergrendelen ingeschakeld No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX contact adres @@ -3632,6 +3675,14 @@ Beschikbaar in v5.1 Bestand verzenden stoppen? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Indienen @@ -4228,6 +4279,10 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link Wanneer beschikbaar No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen. @@ -4320,9 +4375,13 @@ SimpleX Lock moet ingeschakeld zijn. U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken. U verliest uw contacten niet als u deze later verwijdert. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken. No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX Lock moet ingeschakeld zijn. Je ontvangt geen berichten meer van deze groep. Je gesprek geschiedenis blijft behouden. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + U verliest uw contacten niet als u deze later verwijdert. + 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 Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt @@ -4462,7 +4526,7 @@ SimpleX Lock moet ingeschakeld zijn. Your SimpleX contact address - Uw SimpleX contact adres + Uw SimpleX adres No comment provided by engineer. @@ -4532,6 +4596,10 @@ U kunt deze verbinding verbreken en het contact verwijderen (en later proberen m Uw contacten kunnen volledige verwijdering van berichten toestaan. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Uw huidige chat database wordt VERWIJDERD en VERVANGEN door de geïmporteerde. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 78156e1239..3e71f25887 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -361,6 +361,10 @@ Dostępny w v5.1 O SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Kolor akcentu @@ -387,9 +391,8 @@ Dostępny w v5.1 Akceptuj incognito No comment provided by engineer. - - Accept requests - Akceptuj prośby + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Dostępny w v5.1 Dodaj wiadomość powitalną No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Administratorzy mogą tworzyć linki do dołączania do grup. @@ -447,9 +454,12 @@ Dostępny w v5.1 Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie. No comment provided by engineer. - - All your contacts will remain connected - Wszystkie Twoje kontakty pozostaną połączone + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Dostępny w v5.1 Uwierzytelnianie niedostępne No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Automatyczne akceptowanie próśb o kontakt @@ -622,11 +636,6 @@ Dostępny w v5.1 Automatyczne akceptowanie obrazów No comment provided by engineer. - - Automatically - Automatycznie - No comment provided by engineer. - Back Wstecz @@ -992,11 +1001,6 @@ Dostępny w v5.1 Preferencje kontaktu No comment provided by engineer. - - Contact requests - Prośby kontaktu - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć. @@ -1017,9 +1021,12 @@ Dostępny w v5.1 Utwórz No comment provided by engineer. - - Create address - Utwórz adres + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Dostępny w v5.1 Wprowadź serwer ręcznie No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Błąd @@ -2343,6 +2354,10 @@ Dostępny w v5.1 Duży plik! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Opuść @@ -2987,6 +3002,10 @@ Dostępny w v5.1 Hasło profilu No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Zabroń połączeń audio/wideo. @@ -3247,6 +3266,10 @@ Dostępny w v5.1 Zapisz archiwum No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Zapisz profil grupy @@ -3282,6 +3305,10 @@ Dostępny w v5.1 Zapisać serwery? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Zapisać wiadomość powitalną? @@ -3472,6 +3499,14 @@ Dostępny w v5.1 Udostępnij chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Udostępnij link zaproszenia @@ -3487,6 +3522,10 @@ Dostępny w v5.1 Jednorazowy link zaproszenia No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Pokaż kod QR @@ -3537,6 +3576,10 @@ Dostępny w v5.1 Blokada SimpleX włączona No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address Adres kontaktowy SimpleX @@ -3632,6 +3675,14 @@ Dostępny w v5.1 Przestać wysyłać plik? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Zatwierdź @@ -4228,6 +4279,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Gdy dostępny No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi. @@ -4320,9 +4375,13 @@ Funkcja blokady SimpleX musi być włączona. Możesz udostępnić link lub kod QR - każdy będzie mógł dołączyć do grupy. Nie stracisz członków grupy, jeśli później ją usuniesz. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć. Nie stracisz swoich kontaktów, jeśli później go usuniesz. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć. No comment provided by engineer. @@ -4435,6 +4494,11 @@ Funkcja blokady SimpleX musi być włączona. Przestaniesz otrzymywać wiadomości od tej grupy. Historia czatu zostanie zachowana. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Nie stracisz swoich kontaktów, jeśli później go usuniesz. + 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 Próbujesz zaprosić osobę, z którą masz wspólny profil incognito do grupy, w której używasz swojego głównego profilu @@ -4462,7 +4526,7 @@ Funkcja blokady SimpleX musi być włączona. Your SimpleX contact address - Twój adres kontaktowy SimpleX + Twój adres SimpleX No comment provided by engineer. @@ -4532,6 +4596,10 @@ Możesz anulować to połączenie i usunąć kontakt (i spróbować później z Twoje kontakty mogą zezwolić na pełne usunięcie wiadomości. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Twoja obecna baza danych czatu zostanie usunięta i zastąpiona zaimportowaną. 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 6bb8229a32..a0e4c600b1 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -361,6 +361,10 @@ Available in v5.1 Информация о SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color Основной цвет @@ -387,9 +391,8 @@ Available in v5.1 Принять инкогнито No comment provided by engineer. - - Accept requests - Принимать запросы + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Available in v5.1 Добавить приветственное сообщение No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. Админы могут создать ссылки для вступления в группу. @@ -447,9 +454,12 @@ Available in v5.1 Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для Вас. No comment provided by engineer. - - All your contacts will remain connected - Все контакты, которые соединились через этот адрес, сохранятся. + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Available in v5.1 Аутентификация недоступна No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests Автоматически принимать запросы контактов @@ -622,11 +636,6 @@ Available in v5.1 Автоприем изображений No comment provided by engineer. - - Automatically - Автоматически - No comment provided by engineer. - Back Назад @@ -992,11 +1001,6 @@ Available in v5.1 Предпочтения контакта No comment provided by engineer. - - Contact requests - Запросы контактов - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их. @@ -1017,9 +1021,12 @@ Available in v5.1 Создать No comment provided by engineer. - - Create address - Создать адрес + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Available in v5.1 Ввести сервер вручную No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error Ошибка @@ -2343,6 +2354,10 @@ Available in v5.1 Большой файл! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave Выйти @@ -2987,6 +3002,10 @@ Available in v5.1 Пароль профиля No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. Запретить аудио/видео звонки. @@ -3247,6 +3266,10 @@ Available in v5.1 Сохранить архив No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile Сохранить профиль группы @@ -3282,6 +3305,10 @@ Available in v5.1 Сохранить серверы? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? Сохранить приветственное сообщение? @@ -3472,6 +3499,14 @@ Available in v5.1 Поделиться chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link Поделиться ссылкой @@ -3487,6 +3522,10 @@ Available in v5.1 Поделиться ссылкой-приглашением No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code Показать QR код @@ -3537,6 +3576,10 @@ Available in v5.1 Блокировка SimpleX включена No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX ссылка-контакт @@ -3632,6 +3675,14 @@ Available in v5.1 Остановить отправку файла? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit Продолжить @@ -4228,6 +4279,10 @@ To connect, please ask your contact to create another connection link and check Когда возможно No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. @@ -4320,9 +4375,13 @@ SimpleX Lock must be enabled. Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились. + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами. No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX Lock must be enabled. Вы перестанете получать сообщения от этой группы. История чата будет сохранена. No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + Вы сможете удалить адрес, сохранив контакты, которые через него соединились. + 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 Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль @@ -4532,6 +4596,10 @@ You can cancel this connection and remove the contact (and try later with a new Ваши контакты могут разрешить окончательное удаление сообщений. No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index a518d5f499..1a525f62b6 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -361,6 +361,10 @@ Available in v5.1 关于SimpleX Chat No comment provided by engineer. + + About SimpleX address + No comment provided by engineer. + Accent color 色调 @@ -387,9 +391,8 @@ Available in v5.1 接受隐身聊天 No comment provided by engineer. - - Accept requests - 接受请求 + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. @@ -422,6 +425,10 @@ Available in v5.1 添加欢迎信息 No comment provided by engineer. + + Address + No comment provided by engineer. + Admins can create the links to join groups. 管理员可以创建链接以加入群组。 @@ -447,9 +454,12 @@ Available in v5.1 所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。 No comment provided by engineer. - - All your contacts will remain connected - 您的所有联系人将保持连接 + + All your contacts will remain connected. + No comment provided by engineer. + + + All your contacts will remain connected. Profile update will be sent to your contacts. No comment provided by engineer. @@ -612,6 +622,10 @@ Available in v5.1 身份验证不可用 No comment provided by engineer. + + Auto-accept + No comment provided by engineer. + Auto-accept contact requests 自动接受联系人请求 @@ -622,11 +636,6 @@ Available in v5.1 自动接受图片 No comment provided by engineer. - - Automatically - 自动 - No comment provided by engineer. - Back 返回 @@ -992,11 +1001,6 @@ Available in v5.1 联系人偏好设置 No comment provided by engineer. - - Contact requests - 联系人请求 - No comment provided by engineer. - Contacts can mark messages for deletion; you will be able to view them. 联系人可以将信息标记为删除;您将可以查看这些信息。 @@ -1017,9 +1021,12 @@ Available in v5.1 创建 No comment provided by engineer. - - Create address - 创建地址 + + Create SimpleX address + No comment provided by engineer. + + + Create an address to let people connect with you. No comment provided by engineer. @@ -1590,6 +1597,10 @@ Available in v5.1 手动输入服务器 No comment provided by engineer. + + Enter welcome message… (optional) + placeholder + Error 错误 @@ -2343,6 +2354,10 @@ Available in v5.1 大文件! No comment provided by engineer. + + Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/app-settings.md#your-simplex-contact-address). + No comment provided by engineer. + Leave 离开 @@ -2987,6 +3002,10 @@ Available in v5.1 个人资料密码 No comment provided by engineer. + + Profile update will be sent to your contacts. + No comment provided by engineer. + Prohibit audio/video calls. 禁止音频/视频通话。 @@ -3247,6 +3266,10 @@ Available in v5.1 保存存档 No comment provided by engineer. + + Save auto-accept settings + No comment provided by engineer. + Save group profile 保存群组资料 @@ -3282,6 +3305,10 @@ Available in v5.1 保存服务器? No comment provided by engineer. + + Save settings? + No comment provided by engineer. + Save welcome message? 保存欢迎信息? @@ -3472,6 +3499,14 @@ Available in v5.1 分享 chat item action + + Share address + No comment provided by engineer. + + + Share address with contacts? + No comment provided by engineer. + Share invitation link 分享邀请链接 @@ -3487,6 +3522,10 @@ Available in v5.1 分享一次性邀请链接 No comment provided by engineer. + + Share with contacts + No comment provided by engineer. + Show QR code 显示二维码 @@ -3537,6 +3576,10 @@ Available in v5.1 已开启 SimpleX 锁定 No comment provided by engineer. + + SimpleX address + No comment provided by engineer. + SimpleX contact address SimpleX 联系地址 @@ -3632,6 +3675,14 @@ Available in v5.1 停止发送文件? No comment provided by engineer. + + Stop sharing + No comment provided by engineer. + + + Stop sharing address? + No comment provided by engineer. + Submit 提交 @@ -4228,6 +4279,10 @@ To connect, please ask your contact to create another connection link and check 当可用时 No comment provided by engineer. + + When people request to connect, you can accept or reject it. + No comment provided by engineer. + When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. 当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。 @@ -4320,9 +4375,13 @@ SimpleX Lock must be enabled. 您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。 No comment provided by engineer. - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 如果您以后删除它,您不会丢失您的联系人。 + + You can share this address with your contacts to let them connect with **%@**. + No comment provided by engineer. + + + You can share your address as a link or QR code - anybody can connect to you. + 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 No comment provided by engineer. @@ -4435,6 +4494,11 @@ SimpleX Lock must be enabled. 您将停止接收来自该群组的消息。聊天记录将被保留。 No comment provided by engineer. + + You won't lose your contacts if you later delete your address. + 如果您以后删除它,您不会丢失您的联系人。 + 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 您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组 @@ -4532,6 +4596,10 @@ You can cancel this connection and remove the contact (and try later with a new 您的联系人可以允许完全删除消息。 No comment provided by engineer. + + Your contacts will remain connected. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. 您当前的聊天数据库将被删除并替换为导入的数据库。 From ce2225d35520e20519a5275f7d2017beeb6997cd Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 28 Apr 2023 14:11:32 +0400 Subject: [PATCH 11/22] ios: align 1-time link design with new address design (#2337) * ios: align 1-time link design with new address design * update text * layout * bigger font, icon * padding --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../ChatList/ContactConnectionInfo.swift | 27 +++-- .../Views/NewChat/AddContactLearnMore.swift | 28 ++++++ .../Shared/Views/NewChat/AddContactView.swift | 99 ++++++++++++------- .../Shared/Views/NewChat/CreateLinkView.swift | 4 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + 5 files changed, 107 insertions(+), 55 deletions(-) create mode 100644 apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 72be57b3f7..7d850719d6 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -39,6 +39,11 @@ struct ContactConnectionInfo: View { .padding(.bottom, 16) Text(contactConnectionText(contactConnection)) + .padding(.bottom, 16) + + if let connReqInv = contactConnection.connReqInv { + OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInv) + } } .listRowBackground(Color.clear) .listRowSeparator(.hidden) @@ -47,11 +52,7 @@ struct ContactConnectionInfo: View { Section { if contactConnection.groupLinkId == nil { - HStack(spacing: 20) { - Image(systemName: "pencil") - .foregroundColor(.secondary) - .padding(.leading, 6) - .onTapGesture { aliasTextFieldFocused = true } + settingsRow("pencil") { TextField("Set contact name…", text: $localAlias) .autocapitalization(.none) .autocorrectionDisabled(true) @@ -59,20 +60,18 @@ struct ContactConnectionInfo: View { .submitLabel(.done) .onSubmit(setConnectionAlias) } + .onTapGesture { aliasTextFieldFocused = true } } if contactConnection.initiated, let connReqInv = contactConnection.connReqInv { - NavigationLink { - AddContactView(contactConnection: contactConnection, connReqInvitation: connReqInv) - .navigationTitle(CreateLinkTab.oneTime.title) - .navigationBarTitleDisplayMode(.large) - } label: { - Label("Show QR code", systemImage: "qrcode") - .foregroundColor(contactConnection.incognito ? .indigo : .accentColor) - } + oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInv) + } else { + oneTimeLinkLearnMoreButton() } + } + Section { Button(role: .destructive) { alert = .deleteInvitationAlert } label: { @@ -81,7 +80,6 @@ struct ContactConnectionInfo: View { } } } - .listStyle(.insetGrouped) } .alert(item: $alert) { _alert in switch _alert { @@ -96,7 +94,6 @@ struct ContactConnectionInfo: View { } .onAppear { localAlias = contactConnection.localAlias - aliasTextFieldFocused = true } } diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift new file mode 100644 index 0000000000..c746bca825 --- /dev/null +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -0,0 +1,28 @@ +// +// AddContactLearnMore.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 27.04.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct AddContactLearnMore: View { + var body: some View { + List { + VStack(alignment: .leading, spacing: 18) { + Text("To connect, your contact can scan QR code or use the link in the app.") + Text("If you can't meet in person, show QR code in a video call, or share the link.") + Text("Read more in [User Guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/guide/README.md#connect-to-friends).") + } + .listRowBackground(Color.clear) + } + } +} + +struct AddContactLearnMore_Previews: PreviewProvider { + static var previews: some View { + AddContactLearnMore() + } +} diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index fb3011de51..44de3c4987 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -16,50 +16,73 @@ struct AddContactView: View { var connReqInvitation: String var body: some View { - ScrollView { - VStack(alignment: .leading) { - Text("Your contact can scan it from the app.") - .padding(.bottom, 4) - if (contactConnection?.incognito ?? 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) - } - if connReqInvitation != "" { - QRCode(uri: connReqInvitation).padding(.bottom) - } else { - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(2) - .frame(maxWidth: .infinity) - .padding(.vertical) - } - Text("If you can't meet in person, **show QR code in the video call**, or share the link.") - .padding(.bottom) - Button { - showShareSheet(items: [connReqInvitation]) - } label: { - Label("Share invitation link", systemImage: "square.and.arrow.up") - } - .frame(maxWidth: .infinity, alignment: .center) + List { + OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInvitation) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + Section("1-time link") { + oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInvitation) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } .onAppear { chatModel.connReqInv = connReqInvitation } } } +@ViewBuilder func oneTimeLinkSection(contactConnection: PendingContactConnection? = nil, connReqInvitation: String) -> some View { + if connReqInvitation != "" { + QRCode(uri: connReqInvitation) + } else { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(2) + .frame(maxWidth: .infinity) + .padding(.vertical) + } + shareLinkButton(connReqInvitation) + oneTimeLinkLearnMoreButton() +} + +private func shareLinkButton(_ connReqInvitation: String) -> some View { + Button { + showShareSheet(items: [connReqInvitation]) + } label: { + settingsRow("square.and.arrow.up") { + Text("Share 1-time link") + } + } +} + +func oneTimeLinkLearnMoreButton() -> some View { + NavigationLink { + AddContactLearnMore() + .navigationTitle("One-time invitation link") + .navigationBarTitleDisplayMode(.large) + } label: { + settingsRow("info.circle") { + Text("Learn more") + } + } +} + +struct OneTimeLinkProfileText: View { + @EnvironmentObject private var chatModel: ChatModel + var contactConnection: PendingContactConnection? = nil + var connReqInvitation: String + + var body: some View { + HStack { + if (contactConnection?.incognito ?? chatModel.incognito) { + Image(systemName: "theatermasks").foregroundColor(.indigo) + Text("A random profile will be sent to your contact") + } else { + Image(systemName: "info.circle").foregroundColor(.secondary) + Text("Your chat profile will be sent to your contact") + } + } + } +} + struct AddContactView_Previews: PreviewProvider { static var previews: some View { AddContactView(connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D") diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift index f1274ef388..71daf88a8c 100644 --- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift +++ b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift @@ -15,7 +15,7 @@ enum CreateLinkTab { var title: LocalizedStringKey { switch self { case .oneTime: return "One-time invitation link" - case .longTerm: return "Your contact address" + case .longTerm: return "Your SimpleX address" } } } @@ -51,7 +51,7 @@ struct CreateLinkView: View { .tag(CreateLinkTab.oneTime) UserAddressView(viaCreateLinkView: true) .tabItem { - Label("Your contact address", systemImage: "infinity.circle") + Label("Your SimpleX address", systemImage: "infinity.circle") } .tag(CreateLinkTab.longTerm) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2d8170bd79..3d666c7578 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; + 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; @@ -439,6 +440,7 @@ 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; + 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.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 = ""; }; @@ -694,6 +696,7 @@ 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */, 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */, 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */, + 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */, ); path = NewChat; sourceTree = ""; @@ -1087,6 +1090,7 @@ 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */, D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, + 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, From 199835b6710762abec16dfeaebd47c7fa465122c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 28 Apr 2023 20:34:24 +0400 Subject: [PATCH 12/22] ios: allow all group members to view welcome message (#2347) * ios: allow all group members to view welcome message * remove diff * layout * ignore taps on preview * remove newline * show/hide keyboard * fix button flickering * remove space * remove unused function --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../Views/Chat/Group/GroupChatInfoView.swift | 2 +- .../Views/Chat/Group/GroupWelcomeView.swift | 110 +++++++++++++----- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index ed8dfd9221..efbb3c1320 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -45,8 +45,8 @@ struct GroupChatInfoView: View { Section { if groupInfo.canEdit { editGroupButton() - addOrEditWelcomeMessage() } + addOrEditWelcomeMessage() groupPreferencesButton($groupInfo) } header: { Text("") diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 7aa372d5dc..7b90a4abe7 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -15,43 +15,101 @@ struct GroupWelcomeView: View { var groupId: Int64 @Binding var groupInfo: GroupInfo @State private var welcomeText: String = "" + @State private var editMode = true @FocusState private var keyboardVisible: Bool @State private var showSaveDialog = false var body: some View { - List { - Section { - TextEditor(text: $welcomeText) - .focused($keyboardVisible) - .padding(.horizontal, -5) - .padding(.top, -8) - .frame(height: 90, alignment: .topLeading) - .frame(maxWidth: .infinity, alignment: .leading) - } - Section { - saveButton() + VStack { + if groupInfo.canEdit { + editorView() + .modifier(BackButton { + if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) { + dismiss() + } else { + showSaveDialog = true + } + }) + .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { + Button("Save and update group profile") { + save() + dismiss() + } + Button("Exit without saving") { dismiss() } + } + } else { + List { + Section { + textPreview() + copyButton() + } + } } } .onAppear { welcomeText = groupInfo.groupProfile.description ?? "" - } - .modifier(BackButton { - if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) { - dismiss() - } else { - showSaveDialog = true - } - }) - .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { - Button("Save and update group profile") { - save() - dismiss() - } - Button("Exit without saving") { dismiss() } + keyboardVisible = true } } - @ViewBuilder private func saveButton() -> some View { + private func textPreview() -> some View { + messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil) + .allowsHitTesting(false) + } + + private func editorView() -> some View { + List { + Section { + if editMode { + ZStack { + Group { + if welcomeText.isEmpty { + TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message…", comment: "placeholder"))) + .foregroundColor(.secondary) + .disabled(true) + } + TextEditor(text: $welcomeText) + .focused($keyboardVisible) + } + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 90, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) + } + } else { + textPreview() + .frame(height: 90, alignment: .topLeading) + } + + Button { + editMode = !editMode + keyboardVisible = editMode + } label: { + if editMode { + Label ("Preview", systemImage: "character") + } else { + Label ("Edit", systemImage: "pencil") + } + } + .disabled(welcomeText.isEmpty) + copyButton() + } + + Section { + saveButton() + } + } + } + + private func copyButton() -> some View { + Button { + UIPasteboard.general.string = welcomeText + } label: { + Label ("Copy", systemImage: "doc.on.doc") + } + } + + private func saveButton() -> some View { Button("Save and update group profile") { save() } From 00caeae91438b1bdb4ceec6b6a735cff2ab6baa0 Mon Sep 17 00:00:00 2001 From: M Sarmad Qadeer Date: Sat, 29 Apr 2023 13:53:27 +0500 Subject: [PATCH 13/22] website: add simplex reviews section (#2346) * website: add simplex reviews section * website: update review section position & add quality images * website: add dark mode images * website: avoid text selection when hover on logos * website: add layout fix * website: add spaces in simplex review * website: improve margin of simplex review * website: add title to logos in simplex review * titles, order * update images * move images --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- website/src/_includes/hero.html | 39 +++++++++++++++++++---- website/src/css/style.css | 7 ++++ website/src/img/heise-dark.png | Bin 0 -> 32946 bytes website/src/img/heise-light.png | Bin 0 -> 23920 bytes website/src/img/kuketz-blog-dark.png | Bin 0 -> 31933 bytes website/src/img/kuketz-blog-light.png | Bin 0 -> 35606 bytes website/src/img/optout.jpg | Bin 0 -> 10106 bytes website/src/img/privacy-guides-dark.png | Bin 0 -> 59214 bytes website/src/img/privacy-guides-light.png | Bin 0 -> 67994 bytes website/src/img/trail-of-bits-dark.png | Bin 0 -> 25247 bytes website/src/img/trail-of-bits-light.png | Bin 0 -> 24055 bytes 11 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 website/src/img/heise-dark.png create mode 100644 website/src/img/heise-light.png create mode 100644 website/src/img/kuketz-blog-dark.png create mode 100644 website/src/img/kuketz-blog-light.png create mode 100644 website/src/img/optout.jpg create mode 100644 website/src/img/privacy-guides-dark.png create mode 100644 website/src/img/privacy-guides-light.png create mode 100644 website/src/img/trail-of-bits-dark.png create mode 100644 website/src/img/trail-of-bits-light.png diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index 8b3fff748d..56513701c9 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -17,14 +17,39 @@ {{ "hero-overlay-2-textlink" | i18n({}, lang ) | safe }} {{ overlay(hero_overlays.sections[0], lang) }} +
- -
- - - - - + + +
+ +
+ + + + + +
diff --git a/website/src/css/style.css b/website/src/css/style.css index 83fd1a3eb2..ba0397f224 100644 --- a/website/src/css/style.css +++ b/website/src/css/style.css @@ -39,6 +39,13 @@ html { letter-spacing: 0.003em; } +img{ + user-select: none; + -webkit-user-select: none; /* For Safari and older Chrome versions */ + -moz-user-select: none; /* For Firefox */ + -ms-user-select: none; /* For Internet Explorer and Edge */ +} + /* #comparison::before { display: block; content: " "; diff --git a/website/src/img/heise-dark.png b/website/src/img/heise-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c87546720e7e867aeb9a4a33f9c519b0b4a1a153 GIT binary patch literal 32946 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 literal 0 HcmV?d00001 diff --git a/website/src/img/heise-light.png b/website/src/img/heise-light.png new file mode 100644 index 0000000000000000000000000000000000000000..8f5dd51635b5fb730e359953cd008ba425aa36aa GIT binary patch literal 23920 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�%5x2OsM6mIrOlRq67Z+93+iiHh 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 ypo8no|HWheHIDwXK>r{7{lCl~73F4 literal 0 HcmV?d00001 diff --git a/website/src/img/kuketz-blog-dark.png b/website/src/img/kuketz-blog-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7ef797113d939a5ab51e65d83d5fa88454188c GIT binary patch literal 31933 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 NMndsNwU}|}{{x6A@ag~n literal 0 HcmV?d00001 diff --git a/website/src/img/kuketz-blog-light.png b/website/src/img/kuketz-blog-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5940c21993e6def2d674638ac046302785ec1008 GIT binary patch literal 35606 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 literal 0 HcmV?d00001 diff --git a/website/src/img/optout.jpg b/website/src/img/optout.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3500e4d6dbb026ce412eca284e15dd9701955a18 GIT binary patch literal 10106 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 literal 0 HcmV?d00001 diff --git a/website/src/img/trail-of-bits-dark.png b/website/src/img/trail-of-bits-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..46f4e8a91a4e077f290d4e9b9dd366cc897aa65e GIT binary patch literal 25247 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 literal 0 HcmV?d00001 diff --git a/website/src/img/trail-of-bits-light.png b/website/src/img/trail-of-bits-light.png new file mode 100644 index 0000000000000000000000000000000000000000..872133fe4830749eae389b7841df508192bb4246 GIT binary patch literal 24055 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 literal 0 HcmV?d00001 From 315d830357361a0979e0654eff6fefcac357dbc8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 29 Apr 2023 14:11:44 +0200 Subject: [PATCH 14/22] android: export all theme colors (#2348) --- .../java/chat/simplex/app/ui/theme/Theme.kt | 3 ++ .../chat/simplex/app/ui/theme/ThemeManager.kt | 29 ++++++++++--------- .../chat/simplex/app/views/helpers/Util.kt | 5 ++-- .../app/views/usersettings/Appearance.kt | 22 +++++++------- 4 files changed, 32 insertions(+), 27 deletions(-) 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 85b3a0dbdf..118bca224a 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 @@ -153,6 +153,9 @@ data class ThemeOverrides ( } } +@Serializable +data class ThemeData (val colors: ThemeColors) + fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { return if (baseTheme == DefaultTheme.SIMPLEX) { this.background(brush = Brush.linearGradient( 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 index e6646c9bc2..ef9c42fe7c 100644 --- 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 @@ -44,15 +44,19 @@ object ThemeManager { return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) } - fun currentThemeOverrides(darkForSystemTheme: Boolean): ThemeOverrides { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name - } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - return overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) + fun currentThemeData(darkForSystemTheme: Boolean): ThemeData { + val t = currentColors(darkForSystemTheme) + return ThemeData(colors = ThemeColors( + primary = t.colors.primary.toReadableHex(), + primaryVariant = t.colors.primaryVariant.toReadableHex(), + secondary = t.colors.secondary.toReadableHex(), + secondaryVariant = t.colors.secondaryVariant.toReadableHex(), + background = t.colors.background.toReadableHex(), + surface = t.colors.surface.toReadableHex(), + title = t.appColors.title.toReadableHex(), + sentMessage = t.appColors.sentMessage.toReadableHex(), + receivedMessage = t.appColors.receivedMessage.toReadableHex() + )) } // colors, default theme enum, localized name of theme @@ -124,12 +128,11 @@ object ThemeManager { CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } - fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { + fun saveAndApplyThemeData(name: String, theme: ThemeData, darkForSystemTheme: Boolean) { val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[theme.base.name] = prevValue.copy(colors = theme.colors) + val prevValue = overrides[name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) + overrides[name] = prevValue.copy(colors = theme.colors) appPrefs.themeOverrides.set(overrides) - appPrefs.currentTheme.set(theme.base.name) CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 39f7d88932..8b20bafc29 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -37,6 +37,7 @@ import androidx.core.text.HtmlCompat import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.ThemeData import chat.simplex.app.ui.theme.ThemeOverrides import com.charleskorn.kaml.decodeFromStream import kotlinx.coroutines.* @@ -390,10 +391,10 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable } } -fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverrides? { +fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeData? { SimplexApp.context.contentResolver.openInputStream(uri).use { runCatching { - return yaml.decodeFromStream(it!!) + return yaml.decodeFromStream(it!!) }.onFailure { if (withAlertOnException) { AlertManager.shared.showAlertMsg( 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 4b5e65d710..538a470bf1 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 @@ -236,24 +236,22 @@ fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { } SectionSpacer() SectionView { - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { - val context = LocalContext.current - val theme = remember { mutableStateOf(null as String?) } - val exportThemeLauncher = rememberSaveThemeLauncher(context, theme) - SectionItemView({ - val overrides = ThemeManager.currentThemeOverrides(isInDarkTheme) - theme.value = yaml.encodeToString(overrides) - exportThemeLauncher.launch("simplex.theme") - }) { - Text(generalGetString(R.string.export_theme), color = colors.primary) - } + val context = LocalContext.current + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberSaveThemeLauncher(context, theme) + SectionItemView({ + val themeData = ThemeManager.currentThemeData(isInDarkTheme) + theme.value = yaml.encodeToString(themeData) + exportThemeLauncher.launch("simplex.theme") + }) { + Text(generalGetString(R.string.export_theme), color = colors.primary) } val importThemeLauncher = rememberGetContentLauncher { uri: Uri? -> if (uri != null) { val theme = getThemeFromUri(uri) if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + ThemeManager.saveAndApplyThemeData(currentTheme.name, theme, isInDarkTheme) } } } From f97a1fcedfac149b56e50c05e39745d11f95d7db Mon Sep 17 00:00:00 2001 From: M Sarmad Qadeer Date: Mon, 1 May 2023 02:31:23 +0500 Subject: [PATCH 15/22] website: add docs to website (#2080) * website: add fontmatter & improve image URLs where necessary * website: add docs to website * website: add prismjs for code highlighting * website: change npm install position in web.sh * website: fix an image URL in lang/cs/README.md * website: improve image paths in lang/cs/translations.md * website: add responsiveness & improve stylings of docs * website: add dir to navbar in blog & docs * website: remove scroll in mobile dropdown menu * website: remove rfcs & add guide docs to website * website: remove file renaming script from web.sh * website: add menu to docs in nav * website: add hash list & add scroll to headers * website: customize docs frontmatter through JS * website: remove supported_languages.json * website: move merge_translations.js to JS folder * website: add the following changes to docs - add frontmatter to new doc merged from master - add ignoreForWeb property to frontmatter of README.md docs * website: remove package-lock.json from .gitignore * website: add package-lock.json from .gitignore * website: add no docs message to docs dropdown * website: improve the sidebar of docs * website: add revision date to docs * website: add script to add version to docs frontmatter * website: add layout to display message in docs if its version is old * website: improve nav responsiveness * website: remove frontmatter form main README & rfcs * website: remove rfcs from website folder * website: add ignore condition for rfcs in .eleventy * website: remove frontmatter from lang README docs * website: remove README from website's lang docs * website: add guides menu in nav * website: following changes - add docs_dropdown.json - extend reference menu in nav - remove docs menu from nav * website: fix in docs sidebar * website: revert main docs README.md files * website: revert main docs README.md files * website: move scripts out of js that are for build * website: remove displayAt form guide docs * website: create a docs_sidebar.json & shift to that approach * update navigation * website: set navbar * website: add icons to external links * website: change the approach for docs sidebar creation * website: update docs template * website: add some strings to en.json and map them accordingly * remove icon --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .gitignore | 2 +- docs/ANDROID.md | 5 + docs/CLI.md | 5 + docs/CONTRIBUTING.md | 5 + docs/SERVER.md | 5 + docs/SIMPLEX.md | 5 + docs/SQL.md | 5 + docs/TRANSLATIONS.md | 5 + docs/WEBRTC.md | 5 + docs/XFTP-SERVER.md | 4 + docs/guide/README.md | 3 + docs/guide/app-settings.md | 3 + docs/guide/audio-video-calls.md | 3 + docs/guide/chat-profiles.md | 3 + docs/guide/making-connections.md | 3 + docs/guide/managing-data.md | 3 + docs/guide/privacy-security.md | 3 + docs/guide/secret-groups.md | 3 + docs/guide/send-messages.md | 3 + docs/lang/cs/ANDROID.md | 4 + docs/lang/cs/CLI.md | 12 +- docs/lang/cs/CONTRIBUTING.md | 4 + docs/lang/cs/SERVER.md | 6 +- docs/lang/cs/SIMPLEX.md | 4 + docs/lang/cs/SQL.md | 4 + docs/lang/cs/TRANSLATIONS.md | 8 +- docs/lang/cs/WEBRTC.md | 10 +- docs/lang/fr/ANDROID.md | 4 + docs/lang/fr/CLI.md | 4 + docs/lang/fr/CONTRIBUTING.md | 4 + docs/lang/fr/SERVER.md | 4 + docs/lang/fr/SIMPLEX.md | 4 + docs/lang/fr/SQL.md | 4 + docs/lang/fr/TRANSLATIONS.md | 4 + docs/lang/fr/WEBRTC.md | 4 + docs/protocol/simplex-chat.md | 4 + website/.eleventy.js | 121 +++++- website/customize_docs_frontmatter.js | 90 +++++ website/langs/en.json | 24 +- website/merge_translations.js | 8 +- website/package.json | 3 +- website/src/_data/docs_dropdown.json | 32 ++ website/src/_data/docs_sidebar.json | 38 ++ website/src/_data/guide_dropdown.json | 40 ++ website/src/_includes/footer.html | 41 +- website/src/_includes/layouts/article.html | 2 + website/src/_includes/layouts/doc.html | 123 ++++++ website/src/_includes/navbar.html | 151 +++++--- website/src/css/doc.css | 412 +++++++++++++++++++++ website/src/css/prism.min.css | 4 + website/src/css/style.css | 19 +- website/src/js/docs.js | 91 +++++ website/src/js/prism.min.js | 49 +++ website/web.sh | 6 +- 54 files changed, 1321 insertions(+), 94 deletions(-) create mode 100644 website/customize_docs_frontmatter.js create mode 100644 website/src/_data/docs_dropdown.json create mode 100644 website/src/_data/docs_sidebar.json create mode 100644 website/src/_data/guide_dropdown.json create mode 100644 website/src/_includes/layouts/doc.html create mode 100644 website/src/css/doc.css create mode 100644 website/src/css/prism.min.css create mode 100644 website/src/js/docs.js create mode 100644 website/src/js/prism.min.js diff --git a/.gitignore b/.gitignore index d4b4d6cb33..44fd61e359 100644 --- a/.gitignore +++ b/.gitignore @@ -49,8 +49,8 @@ logs/ # for website website/node_modules/ website/src/blog/ +website/src/docs/ website/translations.json -website/src/_data/supported_languages.json website/src/img/images/ website/src/images/ # Generated files diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 2b86ea9c34..55e6ef976f 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -1,3 +1,8 @@ +--- +title: Accessing files in Android app +revision: 07.02.2023 +--- + | 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) | # Accessing files in Android app diff --git a/docs/CLI.md b/docs/CLI.md index 52a5f01471..65de29e601 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,3 +1,8 @@ +--- +title: Terminal CLI +revision: 31.01.2023 +--- + | Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) | # SimpleX Chat terminal (console) app for Linux/MacOS/Windows diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2694396b62..ef58bfec04 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,3 +1,8 @@ +--- +title: Contributing guide +revision: 31.01.2023 +--- + | Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | # Contributing guide diff --git a/docs/SERVER.md b/docs/SERVER.md index 5c53e9add8..07594ca10c 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -1,3 +1,8 @@ +--- +title: Hosting your own SMP Server +revision: 31.01.2023 +--- + | Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | # Hosting your own SMP Server diff --git a/docs/SIMPLEX.md b/docs/SIMPLEX.md index c00adafd20..7ed01efa3c 100644 --- a/docs/SIMPLEX.md +++ b/docs/SIMPLEX.md @@ -1,3 +1,8 @@ +--- +title: SimpleX platform +revision: 07.02.2023 +--- + | Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | # SimpleX platform - motivation and comparison diff --git a/docs/SQL.md b/docs/SQL.md index d320e36042..d5b469050d 100644 --- a/docs/SQL.md +++ b/docs/SQL.md @@ -1,3 +1,8 @@ +--- +title: Accessing messages in the database +revision: 31.01.2023 +--- + | Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/SQL.md), [CZ](/docs/lang/cs/SQL.md) | # Accessing messages in the database diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index e231610382..bb8764b148 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -1,3 +1,8 @@ +--- +title: Contributing translations to SimpleX Chat +revision: 19.03.2023 +--- + | 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md) | # Contributing translations to SimpleX Chat diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index 850a288347..304f29d812 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -1,3 +1,8 @@ +--- +title: Using custom WebRTC ICE servers in SimpleX Chat +revision: 31.01.2023 +--- + | Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | # Using custom WebRTC ICE servers in SimpleX Chat diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index 1359616ed1..567ae63b01 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -1,3 +1,7 @@ +--- +title: Hosting your own XFTP Server +revision: 21.04.2023 +--- # Hosting your own XFTP Server ## Overview diff --git a/docs/guide/README.md b/docs/guide/README.md index 7836b0b406..04e7538968 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -1,3 +1,6 @@ +--- +title: Quick start +--- # SimpleX Chat User Guide The first messaging platform that has no user identifiers of any kind — 100% private by design. diff --git a/docs/guide/app-settings.md b/docs/guide/app-settings.md index 90adf335bf..26817a20f2 100644 --- a/docs/guide/app-settings.md +++ b/docs/guide/app-settings.md @@ -1,3 +1,6 @@ +--- +title: App settings +--- # App settings ## Opening the app settings diff --git a/docs/guide/audio-video-calls.md b/docs/guide/audio-video-calls.md index 23101d2d63..8fc2bf95b2 100644 --- a/docs/guide/audio-video-calls.md +++ b/docs/guide/audio-video-calls.md @@ -1,3 +1,6 @@ +--- +title: Audio & video Calls +--- # Audio and Video Calls SimpleX Chat allows you to make end-to-end encrypted audio and video calls with your contacts via WebRTC. Note: Group calls are not supported at this time. diff --git a/docs/guide/chat-profiles.md b/docs/guide/chat-profiles.md index 9cede5b8c6..d20790037a 100644 --- a/docs/guide/chat-profiles.md +++ b/docs/guide/chat-profiles.md @@ -1,3 +1,6 @@ +--- +title: Chat profiles +--- # Your chat profiles ## Creating additional chat profiles diff --git a/docs/guide/making-connections.md b/docs/guide/making-connections.md index 55465f1f60..0cf46aecde 100644 --- a/docs/guide/making-connections.md +++ b/docs/guide/making-connections.md @@ -1,3 +1,6 @@ +--- +title: Making connections +--- # Connect to somebody in the chat _Work in progress_ diff --git a/docs/guide/managing-data.md b/docs/guide/managing-data.md index 3324189cf7..c524825746 100644 --- a/docs/guide/managing-data.md +++ b/docs/guide/managing-data.md @@ -1,3 +1,6 @@ +--- +title: Managing data +--- # Managing Your Data ## Automatic message deletion diff --git a/docs/guide/privacy-security.md b/docs/guide/privacy-security.md index 43ece140ad..849a17f65d 100644 --- a/docs/guide/privacy-security.md +++ b/docs/guide/privacy-security.md @@ -1,3 +1,6 @@ +--- +title: Privacy & security +--- # Privacy and Security SimpleX Chat default configuration aims to balance privacy, security and convenience. You may want to change the default options. diff --git a/docs/guide/secret-groups.md b/docs/guide/secret-groups.md index 9ecdc7e4c5..5cdbcf86da 100644 --- a/docs/guide/secret-groups.md +++ b/docs/guide/secret-groups.md @@ -1,3 +1,6 @@ +--- +title: Secret groups +--- # Secret groups Secret groups are anonymous and private, they are designed to be hard to track by outsiders. Every message and file gets sent separately to every member in the group, which makes secret groups only fit for smaller groups. diff --git a/docs/guide/send-messages.md b/docs/guide/send-messages.md index 9762e86f43..8292bd5747 100644 --- a/docs/guide/send-messages.md +++ b/docs/guide/send-messages.md @@ -1,3 +1,6 @@ +--- +title: Sending messages +--- # Sending, editing and deleting messages Sending text messages on SimpleX Chat is pretty straightforward, tap on your contact and then tap on text area, type in your message and tap on "Send" button. diff --git a/docs/lang/cs/ANDROID.md b/docs/lang/cs/ANDROID.md index c0fb859cac..38dd105bb5 100644 --- a/docs/lang/cs/ANDROID.md +++ b/docs/lang/cs/ANDROID.md @@ -1,3 +1,7 @@ +--- +title: Přístup k souborům v aplikaci Android +revision: 07.02.2023 +--- | Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/ANDROID.md) | # Přístup k souborům v aplikaci Android diff --git a/docs/lang/cs/CLI.md b/docs/lang/cs/CLI.md index f6b9dbe0ed..731e236661 100644 --- a/docs/lang/cs/CLI.md +++ b/docs/lang/cs/CLI.md @@ -1,3 +1,7 @@ +--- +title: SimpleX Chat terminálová +revision: 31.01.2023 +--- | Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md) | # SimpleX Chat terminálová (konzolová) aplikace pro Linux/MacOS/Windows @@ -174,7 +178,7 @@ Po spuštění chatu budete vyzváni k zadání svého "zobrazovacího jména" a Následující schéma ukazuje, jak se připojit ke kontaktu a poslat mu zprávu:

Po nastavení místního profilu zadejte `/c` (pro `/connect`) pro vytvoření nového spojení a vygenerování pozvánky. Tuto pozvánku odešlete svému kontaktu prostřednictvím jakéhokoli jiného kanálu. @@ -193,7 +197,7 @@ Seznam dostupných příkazů zobrazíte pomocí `/help` v chatu. Skupinu vytvoříte příkazem `/g ` a kontakty do ní přidáte příkazem `/a `. Do skupiny pak můžete posílat zprávy zadáním `# `. Pro další příkazy použijte `/help groups`. -![simplex-chat](../images/groups.gif) +![simplex-chat](/images/groups.gif) > **Upozornění**: skupiny nejsou uloženy na žádném serveru, jsou vedeny jako seznam členů v databázi aplikace, kterým budou zprávy zasílány. @@ -201,7 +205,7 @@ Skupinu vytvoříte příkazem `/g ` a kontakty do ní přidáte příkaz Soubor můžete odeslat kontaktu pomocí `/f @ ` - příjemce jej bude muset před odesláním přijmout. Pro další příkazy použijte `/help files`. -![simplex-chat](../images/files.gif) +![simplex-chat](/images/files.gif) Soubory můžete posílat skupině pomocí `/f # `. @@ -215,7 +219,7 @@ Uživatelská adresa je "dlouhodobá" v tom smyslu, že se jedná o odkaz pro v Pro ostatní příkazy použijte `/help address`. -![simplex-chat](../images/user-addresses.gif) +![simplex-chat](/images/user-addresses.gif) ### Přístup k historii chatu diff --git a/docs/lang/cs/CONTRIBUTING.md b/docs/lang/cs/CONTRIBUTING.md index 7fc14f5a9c..26c746e7d2 100644 --- a/docs/lang/cs/CONTRIBUTING.md +++ b/docs/lang/cs/CONTRIBUTING.md @@ -1,3 +1,7 @@ +--- +title: Průvodce přispíváním +revision: 31.01.2023 +--- | Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CONTRIBUTING.md), [FR](/docs/lang/fr/CONTRIBUTING.md) | # Průvodce přispíváním diff --git a/docs/lang/cs/SERVER.md b/docs/lang/cs/SERVER.md index ba7dc3efd7..1620ec2e1a 100644 --- a/docs/lang/cs/SERVER.md +++ b/docs/lang/cs/SERVER.md @@ -1,3 +1,7 @@ +--- +title: Hostování vlastního serveru SMP +revision: 31.01.2023 +--- | Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) | # Hostování vlastního serveru SMP @@ -327,4 +331,4 @@ Adresu svého serveru můžete také sdílet se svými přáteli tak, že je nec _Upozornění_: pro podporu hesla je třeba mít SMP server verze 4.0. Pokud již máte nasazený server, můžete heslo přidat přidáním do souboru INI serveru. -       +       diff --git a/docs/lang/cs/SIMPLEX.md b/docs/lang/cs/SIMPLEX.md index 2727d2b9d9..33d45cec65 100644 --- a/docs/lang/cs/SIMPLEX.md +++ b/docs/lang/cs/SIMPLEX.md @@ -1,3 +1,7 @@ +--- +title: Platforma SimpleX +revision: 07.02.2023 +--- | Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md) | # Platforma SimpleX - motivace a srovnání diff --git a/docs/lang/cs/SQL.md b/docs/lang/cs/SQL.md index d9ea8f2365..71ef0007c1 100644 --- a/docs/lang/cs/SQL.md +++ b/docs/lang/cs/SQL.md @@ -1,3 +1,7 @@ +--- +title: Přístup ke zprávám v databázi +revision: 31.01.2023 +--- | Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/SQL.md), [FR](/docs/lang/fr/SQL.md) | # Přístup ke zprávám v databázi diff --git a/docs/lang/cs/TRANSLATIONS.md b/docs/lang/cs/TRANSLATIONS.md index 0cdd13a2b0..b260bd12a6 100644 --- a/docs/lang/cs/TRANSLATIONS.md +++ b/docs/lang/cs/TRANSLATIONS.md @@ -1,3 +1,7 @@ +--- +title: Přispívání překladů do SimpleX Chat +revision: 07.02.2023 +--- | Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/TRANSLATIONS.md) | # Přispívání překladů do SimpleX Chat @@ -22,7 +26,7 @@ Tento dokument vznikl proto, abychom tento proces urychlili, a podělili se s v 2. Některé řetězce není třeba překládat, ale přesto je třeba je překopírovat - v uživatelském rozhraní Weblate je k tomu tlačítko: -weblate: zkopírovat zdroj do překladu +weblate: zkopírovat zdroj do překladu 3. Weblate má také automatické návrhy, které mohou proces urychlit. Někdy je lze použít tak, jak jsou, jindy je třeba je upravit - kliknutím je použijete v překladu. @@ -30,7 +34,7 @@ Tento dokument vznikl proto, abychom tento proces urychlili, a podělili se s v 5. Při překladu [aplikace pro iOS](https://hosted.weblate.org/projects/simplex-chat/ios/) je velká část řetězců naprosto stejná - lze je jedním kliknutím překopírovat do sekce glosář. Vizuální nápovědou, že to lze provést, je to, že celý zdrojový řetězec je zvýrazněn žlutě. Mnoho dalších řetězců je velmi podobných, liší se pouze syntaxí interpolace nebo způsobem použití tučného písma - vyžadují minimální úpravy. Existují některé řetězce, které jsou jedinečné pro platformu iOS - ty je třeba přeložit zvlášť. -weblate: automatické návrhy +weblate: automatické návrhy ## Po dokončení překladu diff --git a/docs/lang/cs/WEBRTC.md b/docs/lang/cs/WEBRTC.md index 3a9dc30a86..64ebc91093 100644 --- a/docs/lang/cs/WEBRTC.md +++ b/docs/lang/cs/WEBRTC.md @@ -1,3 +1,7 @@ +--- +title: Použití vlastních serverů WebRTC ICE v SimpleX Chat +revision: 31.01.2023 +--- | Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md) | # Použití vlastních serverů WebRTC ICE v SimpleX Chat @@ -124,7 +128,7 @@ To je vše - nyní můžete uskutečňovat audio a video hovory prostřednictví 2. Do části **Sestavit seznam serverů ICE** přidejte: - . + . - `STUN: stun::` a stiskněte `Add STUN`. - `TURN: turn::`, `Username: `, `Credential: ` a stiskněte `Add TURN` @@ -133,10 +137,10 @@ To je vše - nyní můžete uskutečňovat audio a video hovory prostřednictví 3. Měli byste vidět své servery v sekci **ICE server list**. Pokud je vše správně nastaveno, stiskněte `Start test`: - + 4. V části **Výsledky** byste měli vidět něco takového: - + Pokud výsledky zobrazují kandidáty `srflx` a `relay`, je vše nastaveno správně! diff --git a/docs/lang/fr/ANDROID.md b/docs/lang/fr/ANDROID.md index 6af2de5ee9..905af50152 100644 --- a/docs/lang/fr/ANDROID.md +++ b/docs/lang/fr/ANDROID.md @@ -1,3 +1,7 @@ +--- +title: Accès aux fichiers dans l'application Android +revision: 07.02.2023 +--- | 07.02.2023 | FR, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md) | # Accès aux fichiers dans l'application Android diff --git a/docs/lang/fr/CLI.md b/docs/lang/fr/CLI.md index 289e9d9722..2c4a88e2d7 100644 --- a/docs/lang/fr/CLI.md +++ b/docs/lang/fr/CLI.md @@ -1,3 +1,7 @@ +--- +title: Application de terminal +revision: 31.01.2023 +--- | 31.01.2023 | FR, [EN](/docs/CLI.md), [CZ](/docs/lang/cs/CLI.md) | # Application de terminal (console) SimpleX Chat pour Linux/MacOS/Windows diff --git a/docs/lang/fr/CONTRIBUTING.md b/docs/lang/fr/CONTRIBUTING.md index c9965e0240..81515b09b2 100644 --- a/docs/lang/fr/CONTRIBUTING.md +++ b/docs/lang/fr/CONTRIBUTING.md @@ -1,3 +1,7 @@ +--- +title: Guide pour contribuer +revision: 31.01.2023 +--- | 31.01.2023 | FR, [EN](/docs/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | # Guide pour contribuer diff --git a/docs/lang/fr/SERVER.md b/docs/lang/fr/SERVER.md index 7bbffd772d..6f9c7c5990 100644 --- a/docs/lang/fr/SERVER.md +++ b/docs/lang/fr/SERVER.md @@ -1,3 +1,7 @@ +--- +title: Héberger votre propre serveur SMP +revision: 31.01.2023 +--- | 31.01.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | # Héberger votre propre serveur SMP diff --git a/docs/lang/fr/SIMPLEX.md b/docs/lang/fr/SIMPLEX.md index 10c55ee05b..a7134205d0 100644 --- a/docs/lang/fr/SIMPLEX.md +++ b/docs/lang/fr/SIMPLEX.md @@ -1,3 +1,7 @@ +--- +title: Plateforme SimpleX +revision: 07.02.2023 +--- | 07.02.2023 | FR, [EN](/docs/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | # Plateforme SimpleX - motivation et comparaison diff --git a/docs/lang/fr/SQL.md b/docs/lang/fr/SQL.md index d1f19abd58..d141153d65 100644 --- a/docs/lang/fr/SQL.md +++ b/docs/lang/fr/SQL.md @@ -1,3 +1,7 @@ +--- +title: Accès aux messages de la base de données +revision: 31.01.2023 +--- | 31.01.2023 | FR, [EN](/docs/SQL.md), [CZ](/docs/lang/cs/SQL.md) | # Accès aux messages de la base de données diff --git a/docs/lang/fr/TRANSLATIONS.md b/docs/lang/fr/TRANSLATIONS.md index 7eb7969daa..e85a4a8513 100644 --- a/docs/lang/fr/TRANSLATIONS.md +++ b/docs/lang/fr/TRANSLATIONS.md @@ -1,3 +1,7 @@ +--- +title: Contribuer aux traductions de SimpleX Chat +revision: 19.03.2023 +--- | 19.03.2023 | FR, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md) | # Contribuer aux traductions de SimpleX Chat diff --git a/docs/lang/fr/WEBRTC.md b/docs/lang/fr/WEBRTC.md index 48f529d6ba..e06ca46c18 100644 --- a/docs/lang/fr/WEBRTC.md +++ b/docs/lang/fr/WEBRTC.md @@ -1,3 +1,7 @@ +--- +title: Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat +revision: 31.01.2023 +--- | 31.01.2023 | FR, [EN](/docs/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | # Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index 0dd61d2921..95069c794c 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -1,3 +1,7 @@ +--- +title: SimpleX Chat Protocol +revision: 08.08.2022 +--- DRAFT Revision 0.1, 2022-08-08 Evgeny Poberezkin diff --git a/website/.eleventy.js b/website/.eleventy.js index 7dc872b6aa..07815958e5 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -15,7 +15,7 @@ const globalConfig = { } const translationsDirectoryPath = './langs' -const supportedRoutes = ["blog", "contact", "invitation", ""] +const supportedRoutes = ["blog", "contact", "invitation", "docs", ""] let supportedLangs = [] fs.readdir(translationsDirectoryPath, (err, files) => { if (err) { @@ -26,7 +26,7 @@ fs.readdir(translationsDirectoryPath, (err, files) => { return file.endsWith('.json') && fs.statSync(translationsDirectoryPath + '/' + file).isFile() }) supportedLangs = jsonFileNames.map(file => file.replace('.json', '')) -}); +}) const translations = require("./translations.json") @@ -40,18 +40,41 @@ module.exports = function (ty) { return "en" }) - ty.addShortcode("getlangRoute", (path) => { - const lang = path.split("/")[1] - if (supportedRoutes.includes(lang)) return "" - if (supportedLangs.includes(lang)) return `/${lang}` - return "/en" + ty.addFilter("getlang", (path) => { + const urlParts = path.split("/") + if (urlParts[1] === "docs") { + if (urlParts[2] === "lang") { + return urlParts[3] + } + return "en" + } + else { + if (supportedRoutes.includes(urlParts[1])) return "en" + else if (supportedLangs.includes(urlParts[1])) return urlParts[1] + return "en" + } }) ty.addShortcode("completeRoute", (obj) => { const urlParts = obj.url.split("/") + if (supportedRoutes.includes(urlParts[1])) { if (urlParts[1] == "blog") return `/blog` + + else if (urlParts[1] === "docs") { + if (urlParts[2] === "lang") { + if (obj.lang === "en") + return `/docs/${urlParts.slice(4).join('/')}` + return `/docs/lang/${obj.lang}/${urlParts.slice(4).join('/')}` + } + else { + if (obj.lang === "en") + return `${obj.url}` + return `/docs/lang/${obj.lang}/${urlParts.slice(2).join('/')}` + } + } + else if (obj.lang === "en") return `${obj.url}` return `/${obj.lang}${obj.url}` @@ -73,7 +96,7 @@ module.exports = function (ty) { '*': 'en' }, defaultLocale: 'en', - }); + }) // Keeps the same directory structure. ty.addPassthroughCopy("src/assets/") @@ -87,7 +110,10 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/hero-phone") ty.addPassthroughCopy("src/hero-phone-dark") ty.addPassthroughCopy("src/blog/images") - supportedLangs.forEach(lang => ty.addPassthroughCopy(`src/${lang}/blog/images`)) + ty.addPassthroughCopy("src/docs/*.png") + ty.addPassthroughCopy("src/docs/images") + ty.addPassthroughCopy("src/docs/protocol/diagrams") + ty.addPassthroughCopy("src/docs/protocol/*.json") ty.addPassthroughCopy("src/images") ty.addPassthroughCopy("src/CNAME") ty.addPassthroughCopy("src/.well-known") @@ -96,6 +122,83 @@ module.exports = function (ty) { return collection.getFilteredByGlob('src/blog/*.md').reverse() }) + ty.addCollection('docs', function (collection) { + const docs = collection.getFilteredByGlob('src/docs/**/*.md') + .map(doc => { + return { url: doc.url, title: doc.data.title, inputPath: doc.inputPath } + }) + + let referenceContent = fs.readFileSync(path.resolve(__dirname, 'src/_data/docs_sidebar.json'), 'utf-8') + referenceContent = JSON.parse(referenceContent).items + + const newDocs = [] + + referenceContent.forEach(referenceMenu => { + referenceMenu.data.forEach(referenceSubmenu => { + docs.forEach(doc => { + const url = doc.url.replace("/docs/", "") + const urlParts = url.split("/") + + if (doc.inputPath.includes(referenceSubmenu)) { + if (urlParts.length === 1 && urlParts[0] !== "") { + const index = newDocs.findIndex((ele) => ele.lang === 'en' && ele.menu === referenceMenu.menu) + if (index !== -1) { + newDocs[index].data.push(doc) + } + else { + newDocs.push({ + lang: 'en', + menu: referenceMenu.menu, + data: [doc], + }) + } + } + else if (urlParts.length > 1 && urlParts[0] !== "" && urlParts[0] !== "lang") { + const index = newDocs.findIndex((ele) => ele.lang === 'en' && ele.menu === referenceMenu.menu) + if (index !== -1) { + newDocs[index].data.push(doc) + } else { + newDocs.push({ + lang: 'en', + menu: referenceMenu.menu, + data: [doc], + }) + } + } + else if (urlParts.length === 3 && urlParts[0] === "lang" && urlParts[2] !== '') { + const index = newDocs.findIndex((ele) => ele.lang === urlParts[1] && ele.menu === referenceMenu.menu) + if (index !== -1) { + newDocs[index].data.push(doc) + } + else { + newDocs.push({ + lang: urlParts[1], + menu: referenceMenu.menu, + data: [doc], + }) + } + } + else if (urlParts.length > 3 && urlParts[0] === "lang" && urlParts[2] !== '') { + const index = newDocs.findIndex((ele) => ele.lang === urlParts[1] && ele.menu === referenceMenu.menu) + if (index !== -1) { + newDocs[index].data.push(doc) + } + else { + newDocs.push({ + lang: urlParts[1], + menu: referenceMenu.menu, + data: [doc], + }) + } + } + } + }) + }) + }) + + return newDocs + }) + ty.addWatchTarget("src/css") ty.addWatchTarget("markdown/") ty.addWatchTarget("components/Card.js") diff --git a/website/customize_docs_frontmatter.js b/website/customize_docs_frontmatter.js new file mode 100644 index 0000000000..8f2546e168 --- /dev/null +++ b/website/customize_docs_frontmatter.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); +const matter = require('gray-matter'); + +const directoryPath = path.resolve(__dirname, 'src/docs'); +const langFolder = 'lang'; +const enFiles = {}; + +function traverseDirectory(directory, currentLanguage = 'en', result = {}, callback) { + const filesAndDirectories = fs.readdirSync(directory); + + filesAndDirectories.forEach((fileOrDirectoryName) => { + const fullPath = path.join(directory, fileOrDirectoryName); + + if (fs.statSync(fullPath).isDirectory()) { + // If the subdirectory is inside the 'lang' folder, update the current language + if (directory.endsWith('/lang')) { + currentLanguage = fileOrDirectoryName; + } + + // Recursively traverse the subdirectories + traverseDirectory(fullPath, currentLanguage, result, callback); + } else { + // Process the file only if it has the '.md' extension + if (path.extname(fullPath) === '.md') { + // Add the language to the file's language array or create a new array if it doesn't exist + const fileName = path.basename(fullPath, '.md'); + if (!result[fileName]) { + result[fileName] = []; + } + result[fileName].push(currentLanguage); + } + if (callback) { + callback(fullPath, currentLanguage); + } + } + }); + + return result; +} + +const fileLanguageMapping = traverseDirectory(directoryPath); + +// Update the frontmatter of each Markdown file +Object.entries(fileLanguageMapping).forEach(([fileName, languages]) => { + // Find and update the frontmatter of each Markdown file + traverseDirectory(directoryPath, null, {}, (fullPath, currentLanguage) => { + if (path.basename(fullPath) === `${fileName}.md`) { + // Read the existing frontmatter + const fileContent = fs.readFileSync(fullPath, 'utf-8'); + const parsedMatter = matter(fileContent); + const relativePath = path.relative(directoryPath, fullPath); + + // Calculate the permalink based on the file's location + const linkPath = path.relative(directoryPath, fullPath).replace(/\.md$/, '.html'); + const permalink = `/docs/${linkPath}`.toLowerCase(); + parsedMatter.data.permalink = permalink; + + // Update the frontmatter with the new languages list + parsedMatter.data.supportedLangsForDoc = languages; + + // Add the layout value + parsedMatter.data.layout = 'layouts/doc.html'; + + if (fullPath.startsWith(path.join(directoryPath, langFolder))) { + // Non-English files + const [language, ...rest] = relativePath.split(path.sep).slice(1); + const enFilePath = path.join(directoryPath, ...rest); + + if (enFiles[enFilePath]) { + const enRevision = new Date(enFiles[enFilePath].revision); + const currentRevision = new Date(parsedMatter.data.revision); + + const isOld = currentRevision < enRevision; + // Add the version value + parsedMatter.data.version = isOld ? 'old' : 'new'; + } + } else { + // English files + enFiles[fullPath] = { revision: parsedMatter.data.revision }; + // Add the version value + parsedMatter.data.version = 'new'; + } + + // Save the updated frontmatter and content back to the file + const updatedFileContent = matter.stringify(parsedMatter.content, parsedMatter.data); + fs.writeFileSync(fullPath, updatedFileContent, 'utf-8'); + } + }); +}); diff --git a/website/langs/en.json b/website/langs/en.json index 1a8b0baa85..7b0f35b1c1 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -209,5 +209,27 @@ "comparison-section-list-point-5": "Does not protect users' metadata", "comparison-section-list-point-6": "While P2P are distributed, they are not federated - they operate as a single network", "comparison-section-list-point-7": "P2P networks either have a central authority or the whole network can be compromised", - "see-here": "see here" + "see-here": "see here", + "guide-dropdown-1": "Quick start", + "guide-dropdown-2": "Sending messages", + "guide-dropdown-3": "Secret groups", + "guide-dropdown-4": "Chat profiles", + "guide-dropdown-5": "Managing data", + "guide-dropdown-6": "Audio & video Calls", + "guide-dropdown-7": "Privacy & security", + "guide-dropdown-8": "App settings", + "guide-dropdown-9": "Making connections", + "guide": "Guide", + "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-2": "Accessing Android files", + "docs-dropdown-3": "Accessing chat database", + "docs-dropdown-4": "Host SMP Server", + "docs-dropdown-5": "Host XFTP Server", + "docs-dropdown-6": "WebRTC servers", + "docs-dropdown-7": "Translate SimpleX Chat", + "newer-version-of-eng-msg": "There is a newer version of this page in English.", + "click-to-see": "Click to see", + "menu": "Menu", + "on-this-page": "On this page", + "back-to-top": "Back to top" } \ No newline at end of file diff --git a/website/merge_translations.js b/website/merge_translations.js index 37de897de8..163606d5e1 100644 --- a/website/merge_translations.js +++ b/website/merge_translations.js @@ -9,11 +9,11 @@ const jsonFileNames = files.filter(file => file.endsWith('.json')); supportedLangs = jsonFileNames.map(file => file.replace('.json', '')) // keys of the english language are used as the base keys -const enStrings = require("./langs/en.json") +const enStrings = require(path.resolve(__dirname, "langs/en.json")) const languages = {} for (const lang of supportedLangs) { - languages[lang] = require(`./langs/${lang}.json`) + languages[lang] = require(path.resolve(__dirname, `langs/${lang}.json`)) } // this program generates a combined translations.json file @@ -27,9 +27,7 @@ for (const key in enStrings) { translations[key] = langStrings } -saveFile("translations.json", translations) -// the list in the supported_languages.json file is used as the reference list for displaying available languages on the frontend -saveFile("src/_data/supported_languages.json", {"langs": supportedLangs}) +saveFile(path.resolve(__dirname, "translations.json"), translations) function saveFile(relPath, data) { filePath = path.resolve(__dirname, relPath) diff --git a/website/package.json b/website/package.json index 5560e7bece..758e2f62a5 100644 --- a/website/package.json +++ b/website/package.json @@ -28,6 +28,7 @@ "tailwindcss": "^3.2.7" }, "dependencies": { - "eleventy-plugin-i18n": "^0.1.3" + "eleventy-plugin-i18n": "^0.1.3", + "gray-matter": "^4.0.3" } } diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json new file mode 100644 index 0000000000..ff05242c81 --- /dev/null +++ b/website/src/_data/docs_dropdown.json @@ -0,0 +1,32 @@ +{ + "items": [ + { + "title": "docs-dropdown-1", + "url": "/docs/simplex.html" + }, + { + "title": "docs-dropdown-2", + "url": "/docs/android.html" + }, + { + "title": "docs-dropdown-3", + "url": "/docs/sql.html" + }, + { + "title": "docs-dropdown-4", + "url": "/docs/server.html" + }, + { + "title": "docs-dropdown-5", + "url": "/docs/xftp-server.html" + }, + { + "title": "docs-dropdown-6", + "url": "/docs/webrtc.html" + }, + { + "title": "docs-dropdown-7", + "url": "/docs/translations.html" + } + ] +} \ No newline at end of file diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json new file mode 100644 index 0000000000..f05ae0ab1a --- /dev/null +++ b/website/src/_data/docs_sidebar.json @@ -0,0 +1,38 @@ +{ + "items": [ + { + "menu": "Guide", + "data": [ + "README.md", + "send-messages.md", + "secret-groups.md", + "chat-profiles.md", + "managing-data.md", + "audio-video-calls.md", + "privacy-security.md", + "app-settings.md", + "making-connections.md" + ] + }, + { + "menu": "Reference", + "data": [ + "SIMPLEX.md", + "ANDROID.md", + "CLI.md", + "SQL.md", + "CONTRIBUTING.md", + "SERVER.md", + "TRANSLATIONS.md", + "WEBRTC.md", + "XFTP-SERVER.md" + ] + }, + { + "menu": "Protocol", + "data": [ + "simplex-chat.md" + ] + } + ] +} \ No newline at end of file diff --git a/website/src/_data/guide_dropdown.json b/website/src/_data/guide_dropdown.json new file mode 100644 index 0000000000..fc6383c709 --- /dev/null +++ b/website/src/_data/guide_dropdown.json @@ -0,0 +1,40 @@ +{ + "items": [ + { + "title": "guide-dropdown-1", + "url": "/docs/guide/readme.html" + }, + { + "title": "guide-dropdown-2", + "url": "/docs/guide/send-messages.html" + }, + { + "title": "guide-dropdown-3", + "url": "/docs/guide/secret-groups.html" + }, + { + "title": "guide-dropdown-4", + "url": "/docs/guide/chat-profiles.html" + }, + { + "title": "guide-dropdown-5", + "url": "/docs/guide/managing-data.html" + }, + { + "title": "guide-dropdown-6", + "url": "/docs/guide/audio-video-calls.html" + }, + { + "title": "guide-dropdown-7", + "url": "/docs/guide/privacy-security.html" + }, + { + "title": "guide-dropdown-8", + "url": "/docs/guide/app-settings.html" + }, + { + "title": "guide-dropdown-9", + "url": "/docs/guide/making-connections.html" + } + ] +} \ No newline at end of file diff --git a/website/src/_includes/footer.html b/website/src/_includes/footer.html index a3b984be38..42d8591fda 100644 --- a/website/src/_includes/footer.html +++ b/website/src/_includes/footer.html @@ -1,38 +1,51 @@ +{% set lang = page.url | getlang %} {% include "footer.html" %} + diff --git a/website/src/_includes/layouts/doc.html b/website/src/_includes/layouts/doc.html new file mode 100644 index 0000000000..04b4b99928 --- /dev/null +++ b/website/src/_includes/layouts/doc.html @@ -0,0 +1,123 @@ +{% set lang = page.url | getlang %} + + + + + + + + {{ title }} + + + + + + + + + + + +
+ {% include "navbar.html" %} +
+ +
+
+
+ + +
+
+
+ + {% include "footer.html" %} + + + + + + \ No newline at end of file diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index a756701416..49836649e1 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -1,16 +1,24 @@ +{% set lang = page.url | getlang %} +{% set langName = "aaa" %} +{% for language in languages.languages %} + {% if language.label == page.url | getlang %} + {% set langName = language.name %} + {% endif %} +{% endfor %} +
- - - - + + + +
- logo - + logo +
- SimpleX Whitepaper + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + SimpleX Whitepaper + + - {{ "simplex-chat-protocol" | i18n({}, lang ) | safe }} + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + {{ "simplex-chat-protocol" | i18n({}, lang ) | safe }} + + - {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} + + - {{ "donate" | i18n({}, lang ) | safe }} + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + {{ "donate" | i18n({}, lang ) | safe }} + +
- {{ "terminal-cli" | i18n({}, lang ) | safe }} + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + {{ "terminal-cli" | i18n({}, lang ) | safe }} + + - TypeScript SDK + class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> + TypeScript SDK + +
diff --git a/website/src/_includes/layouts/article.html b/website/src/_includes/layouts/article.html index 922d9905a1..a4cf610ae3 100644 --- a/website/src/_includes/layouts/article.html +++ b/website/src/_includes/layouts/article.html @@ -25,6 +25,7 @@ + @@ -41,6 +42,7 @@