mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-11 13:15:05 +00:00
Merge branch 'master' into ab/diff-subs
This commit is contained in:
@@ -927,14 +927,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
|
||||
}
|
||||
}
|
||||
|
||||
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
||||
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(
|
||||
fileId: fileId,
|
||||
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
|
||||
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
|
||||
auto: auto
|
||||
) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
||||
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
@@ -947,19 +952,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B
|
||||
}
|
||||
} else if let networkErrorAlert = networkErrorAlert(r) {
|
||||
logger.error("apiReceiveFile network error: \(String(describing: r))")
|
||||
am.showAlert(networkErrorAlert)
|
||||
if !auto {
|
||||
am.showAlert(networkErrorAlert)
|
||||
}
|
||||
} else {
|
||||
switch chatError(r) {
|
||||
case .fileCancelled:
|
||||
logger.debug("apiReceiveFile ignoring fileCancelled error")
|
||||
case .fileAlreadyReceiving:
|
||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||
case let .fileNotApproved(fileId, unknownServers):
|
||||
logger.debug("apiReceiveFile fileNotApproved error")
|
||||
if !auto {
|
||||
let srvs = unknownServers.map { s in
|
||||
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
|
||||
srv.hostnames[0]
|
||||
} else {
|
||||
serverHost(s)
|
||||
}
|
||||
}
|
||||
am.showAlert(Alert(
|
||||
title: Text("Unknown servers!"),
|
||||
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
|
||||
primaryButton: .default(
|
||||
Text("Download"),
|
||||
action: {
|
||||
Task {
|
||||
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
default:
|
||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||
am.showAlertMsg(
|
||||
title: "Error receiving file",
|
||||
message: "Error: \(String(describing: r))"
|
||||
)
|
||||
if !auto {
|
||||
am.showAlertMsg(
|
||||
title: "Error receiving file",
|
||||
message: "Error: \(String(describing: r))"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func serverHost(_ s: String) -> String {
|
||||
func serverHost(_ s: String) -> String {
|
||||
if let i = s.range(of: "@")?.lowerBound {
|
||||
return String(s[i...].dropFirst())
|
||||
} else {
|
||||
|
||||
@@ -60,6 +60,7 @@ struct CIFileView: View {
|
||||
case .rcvInvitation: return true
|
||||
case .rcvAccepted: return true
|
||||
case .rcvTransfer: return false
|
||||
case .rcvAborted: return true
|
||||
case .rcvComplete: return true
|
||||
case .rcvCancelled: return false
|
||||
case .rcvError: return false
|
||||
@@ -73,10 +74,10 @@ struct CIFileView: View {
|
||||
logger.debug("CIFileView fileAction")
|
||||
if let file = file {
|
||||
switch (file.fileStatus) {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
if fileSizeValid(file) {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
@@ -148,6 +149,8 @@ struct CIFileView: View {
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted:
|
||||
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
|
||||
case .rcvComplete: fileIcon("doc.fill")
|
||||
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
|
||||
@@ -17,7 +17,6 @@ struct CIImageView: View {
|
||||
let maxWidth: CGFloat
|
||||
@Binding var imgWidth: CGFloat?
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State var metaColor: Color
|
||||
@State private var showFullScreenImage = false
|
||||
|
||||
var body: some View {
|
||||
@@ -38,7 +37,7 @@ struct CIImageView: View {
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
@@ -103,6 +102,7 @@ struct CIImageView: View {
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case .rcvTransfer: progressView()
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
@@ -116,7 +116,7 @@ struct CIImageView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(metaColor)
|
||||
.foregroundColor(.white)
|
||||
.padding(padding)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ struct CIVideoView: View {
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
case .rcvAccepted:
|
||||
switch file.fileProtocol {
|
||||
@@ -95,7 +95,7 @@ struct CIVideoView: View {
|
||||
}
|
||||
durationProgress()
|
||||
}
|
||||
if let file = file, case .rcvInvitation = file.fileStatus {
|
||||
if let file = file, showDownloadButton(file.fileStatus) {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
@@ -105,6 +105,14 @@ struct CIVideoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
|
||||
switch fileStatus {
|
||||
case .rcvInvitation: true
|
||||
case .rcvAborted: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .center) {
|
||||
@@ -280,6 +288,7 @@ struct CIVideoView: View {
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
@@ -318,10 +327,10 @@ struct CIVideoView: View {
|
||||
}
|
||||
|
||||
// TODO encrypt: where file size is checked?
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user, file.fileId, false)
|
||||
await receiveFile(user, file.fileId, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +139,10 @@ struct VoiceMessagePlayer: View {
|
||||
case .sndComplete: playbackButton()
|
||||
case .sndCancelled: playbackButton()
|
||||
case .sndError: playbackButton()
|
||||
case .rcvInvitation: downloadButton(recordingFile)
|
||||
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
||||
case .rcvAccepted: loadingIcon()
|
||||
case .rcvTransfer: loadingIcon()
|
||||
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
|
||||
case .rcvComplete: playbackButton()
|
||||
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
@@ -217,7 +218,7 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadButton(_ recordingFile: CIFile) -> some View {
|
||||
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
if let user = chatModel.currentUser {
|
||||
@@ -225,7 +226,7 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
switch (chatItem.content.msgContent) {
|
||||
case let .image(text, image):
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor)
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
|
||||
@@ -34,8 +34,7 @@ struct GroupPreferencesView: View {
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
||||
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
||||
// TODO enable simplexLinks preference in 5.8
|
||||
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.history, $preferences.history.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
@@ -102,8 +101,6 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
// remove in v5.8
|
||||
.disabled(true)
|
||||
}
|
||||
} else {
|
||||
settingsRow(icon, color: color) {
|
||||
|
||||
@@ -23,6 +23,7 @@ extension AppSettings {
|
||||
setNetCfg(val)
|
||||
}
|
||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
|
||||
if let val = privacyAcceptImages {
|
||||
privacyAcceptImagesGroupDefault.set(val)
|
||||
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
@@ -50,6 +51,7 @@ extension AppSettings {
|
||||
var c = AppSettings.defaults
|
||||
c.networkConfig = getNetCfg()
|
||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||
|
||||
@@ -82,29 +82,29 @@ struct NetworkAndServers: View {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
|
||||
// Section {
|
||||
// Picker("Private routing", selection: $proxyMode) {
|
||||
// ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Picker("Allow downgrade", selection: $proxyFallback) {
|
||||
// ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||
// }
|
||||
// .disabled(proxyMode == .never)
|
||||
// .frame(height: 36)
|
||||
//
|
||||
// Toggle("Show message status", isOn: $showSentViaProxy)
|
||||
// } header: {
|
||||
// Text("Private message routing")
|
||||
// } footer: {
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||
// if showSentViaProxy {
|
||||
// Text("Show → on messages sent via private routing.")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Section {
|
||||
Picker("Private routing", selection: $proxyMode) {
|
||||
ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.frame(height: 36)
|
||||
|
||||
Picker("Allow downgrade", selection: $proxyFallback) {
|
||||
ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.disabled(proxyMode == .never)
|
||||
.frame(height: 36)
|
||||
|
||||
Toggle("Show message status", isOn: $showSentViaProxy)
|
||||
} header: {
|
||||
Text("Private message routing")
|
||||
} footer: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||
if showSentViaProxy {
|
||||
Text("Show → on messages sent via private routing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Calls") {
|
||||
NavigationLink {
|
||||
|
||||
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
|
||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -64,18 +65,6 @@ struct PrivacySettings: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("lock.doc") {
|
||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||
.onChange(of: encryptLocalFiles) {
|
||||
setEncryptLocalFiles($0)
|
||||
}
|
||||
}
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
settingsRow("network") {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
}
|
||||
@@ -108,6 +97,32 @@ struct PrivacySettings: View {
|
||||
Text("Chats")
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("lock.doc") {
|
||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||
.onChange(of: encryptLocalFiles) {
|
||||
setEncryptLocalFiles($0)
|
||||
}
|
||||
}
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
settingsRow("network.badge.shield.half.filled") {
|
||||
Toggle("Protect IP address", isOn: $askToApproveRelays)
|
||||
}
|
||||
} header: {
|
||||
Text("Files")
|
||||
} footer: {
|
||||
if askToApproveRelays {
|
||||
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
|
||||
} else {
|
||||
Text("Without Tor or VPN, your IP address will be visible to file servers.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("person") {
|
||||
Toggle("Contacts", isOn: $contactReceipts)
|
||||
|
||||
@@ -696,14 +696,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
||||
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
||||
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted))
|
||||
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
|
||||
if case .cmdOk = r { return }
|
||||
logger.error("setFileToReceive error: \(responseError(r))")
|
||||
}
|
||||
|
||||
@@ -76,11 +76,6 @@
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */; };
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */; };
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC92BF7A6900003B86B /* libgmp.a */; };
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */; };
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCB2BF7A6900003B86B /* libffi.a */; };
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
|
||||
@@ -144,6 +139,11 @@
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||
5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */; };
|
||||
5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87902C024F4F00583B8A /* libgmp.a */; };
|
||||
5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87912C024F4F00583B8A /* libgmpxx.a */; };
|
||||
5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87922C024F4F00583B8A /* libffi.a */; };
|
||||
5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */; };
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
@@ -354,11 +354,6 @@
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
|
||||
@@ -440,6 +435,11 @@
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||
5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5CEE87902C024F4F00583B8A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CEE87912C024F4F00583B8A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CEE87922C024F4F00583B8A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a"; sourceTree = "<group>"; };
|
||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
@@ -529,13 +529,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */,
|
||||
5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */,
|
||||
5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */,
|
||||
5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */,
|
||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */,
|
||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */,
|
||||
5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */,
|
||||
5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -601,11 +601,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */,
|
||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */,
|
||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */,
|
||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */,
|
||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */,
|
||||
5CEE87922C024F4F00583B8A /* libffi.a */,
|
||||
5CEE87902C024F4F00583B8A /* libgmp.a */,
|
||||
5CEE87912C024F4F00583B8A /* libgmpxx.a */,
|
||||
5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */,
|
||||
5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1552,7 +1552,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1601,7 +1601,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1687,7 +1687,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -1724,7 +1724,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -1761,7 +1761,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1812,7 +1812,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 217;
|
||||
CURRENT_PROJECT_VERSION = 220;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
||||
@@ -123,8 +123,8 @@ public enum ChatCommand {
|
||||
case apiGetNetworkStatuses
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64, encrypted: Bool?)
|
||||
case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
|
||||
case cancelFile(fileId: Int64)
|
||||
// remote desktop commands
|
||||
case setLocalDeviceName(displayName: String)
|
||||
@@ -282,8 +282,8 @@ public enum ChatCommand {
|
||||
case .apiGetNetworkStatuses: return "/_network_statuses"
|
||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))"
|
||||
case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||
case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
|
||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
|
||||
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
|
||||
@@ -1760,6 +1760,7 @@ public enum ChatErrorType: Decodable {
|
||||
case fileImageType(filePath: String)
|
||||
case fileImageSize(filePath: String)
|
||||
case fileNotReceived(fileId: Int64)
|
||||
case fileNotApproved(fileId: Int64, unknownServers: [String])
|
||||
// case xFTPRcvFile
|
||||
// case xFTPSndFile
|
||||
case fallbackToSMPProhibited(fileId: Int64)
|
||||
@@ -2038,6 +2039,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAskToApproveRelays: Bool? = nil
|
||||
public var privacyAcceptImages: Bool? = nil
|
||||
public var privacyLinkPreviews: Bool? = nil
|
||||
public var privacyShowChatPreviews: Bool? = nil
|
||||
@@ -2061,6 +2063,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
let def = AppSettings.defaults
|
||||
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
@@ -2085,6 +2088,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
AppSettings (
|
||||
networkConfig: NetCfg.defaults,
|
||||
privacyEncryptLocalFiles: true,
|
||||
privacyAskToApproveRelays: true,
|
||||
privacyAcceptImages: true,
|
||||
privacyLinkPreviews: true,
|
||||
privacyShowChatPreviews: true,
|
||||
|
||||
@@ -23,6 +23,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||
public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
@@ -73,6 +74,7 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||
GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true,
|
||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
||||
@@ -181,6 +183,8 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults
|
||||
|
||||
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
||||
|
||||
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
|
||||
@@ -3174,6 +3174,7 @@ public struct CIFile: Decodable {
|
||||
case .rcvInvitation: return false
|
||||
case .rcvAccepted: return false
|
||||
case .rcvTransfer: return false
|
||||
case .rcvAborted: return false
|
||||
case .rcvCancelled: return false
|
||||
case .rcvComplete: return true
|
||||
case .rcvError: return false
|
||||
@@ -3198,6 +3199,7 @@ public struct CIFile: Decodable {
|
||||
case .rcvInvitation: return nil
|
||||
case .rcvAccepted: return rcvCancelAction
|
||||
case .rcvTransfer: return rcvCancelAction
|
||||
case .rcvAborted: return nil
|
||||
case .rcvCancelled: return nil
|
||||
case .rcvComplete: return nil
|
||||
case .rcvError: return nil
|
||||
@@ -3312,6 +3314,7 @@ public enum CIFileStatus: Decodable, Equatable {
|
||||
case rcvInvitation
|
||||
case rcvAccepted
|
||||
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
||||
case rcvAborted
|
||||
case rcvComplete
|
||||
case rcvCancelled
|
||||
case rcvError
|
||||
@@ -3327,6 +3330,7 @@ public enum CIFileStatus: Decodable, Equatable {
|
||||
case .rcvInvitation: return "rcvInvitation"
|
||||
case .rcvAccepted: return "rcvAccepted"
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
|
||||
case .rcvAborted: return "rcvAborted"
|
||||
case .rcvComplete: return "rcvComplete"
|
||||
case .rcvCancelled: return "rcvCancelled"
|
||||
case .rcvError: return "rcvError"
|
||||
|
||||
@@ -7,7 +7,7 @@ import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.initChatControllerAndRunMigrations
|
||||
import chat.simplex.common.platform.initChatControllerOnStart
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Date
|
||||
@@ -60,7 +60,7 @@ class MessagesFetcherWork(
|
||||
try {
|
||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
initChatControllerOnStart()
|
||||
}
|
||||
withTimeout(durationSeconds * 1000L) {
|
||||
val chatController = ChatController
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context
|
||||
import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -66,6 +65,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
context = this
|
||||
initHaskell()
|
||||
initMultiplatform()
|
||||
runMigrations()
|
||||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
|
||||
@@ -74,7 +74,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
// It's important, otherwise, user may be locked in undefined state
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
initChatControllerOnStart()
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
}
|
||||
@@ -254,7 +254,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
override fun androidSetNightModeIfSupported() {
|
||||
if (Build.VERSION.SDK_INT < 31) return
|
||||
|
||||
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) {
|
||||
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
|
||||
null
|
||||
} else {
|
||||
CurrentColors.value.colors.isLight
|
||||
|
||||
@@ -77,7 +77,7 @@ class SimplexService: Service() {
|
||||
isServiceStarted = true
|
||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||
initChatControllerAndRunMigrations()
|
||||
initChatControllerOnStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ kotlin {
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation(kotlin("test-common"))
|
||||
implementation(kotlin("test-annotations-common"))
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
|
||||
+2
@@ -15,8 +15,10 @@ actual val dataDir: File = androidAppContext.dataDir
|
||||
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
|
||||
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files")
|
||||
actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files")
|
||||
actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers").also { it.mkdirs() }
|
||||
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
|
||||
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"
|
||||
actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs")
|
||||
|
||||
actual val chatDatabaseFileName: String = "files_chat.db"
|
||||
actual val agentDatabaseFileName: String = "files_agent.db"
|
||||
|
||||
+7
@@ -7,6 +7,8 @@ import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.text.BidiFormatter
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.Font
|
||||
@@ -14,9 +16,11 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.common.model.AppPreferences
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.SharedPreferencesSettings
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.desc.desc
|
||||
|
||||
@@ -51,3 +55,6 @@ actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
||||
|
||||
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
|
||||
getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap()
|
||||
|
||||
+8
-23
@@ -19,14 +19,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.common.R
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -34,7 +30,6 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.saveAppLocale
|
||||
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -46,9 +41,8 @@ enum class AppIcon(val image: ImageResource) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
|
||||
actual fun AppearanceView(m: ChatModel) {
|
||||
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
||||
|
||||
fun setAppIcon(newIcon: AppIcon) {
|
||||
if (appIcon.value == newIcon) return
|
||||
val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
|
||||
@@ -65,18 +59,11 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
|
||||
|
||||
appIcon.value = newIcon
|
||||
}
|
||||
|
||||
AppearanceScope.AppearanceLayout(
|
||||
appIcon,
|
||||
m.controller.appPrefs.appLanguage,
|
||||
m.controller.appPrefs.systemDarkTheme,
|
||||
changeIcon = ::setAppIcon,
|
||||
showSettingsModal = showSettingsModal,
|
||||
editColor = { name, initialColor ->
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ColorEditor(name, initialColor, close)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,8 +73,6 @@ fun AppearanceScope.AppearanceLayout(
|
||||
languagePref: SharedPreference<String?>,
|
||||
systemDarkTheme: SharedPreference<String?>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
editColor: (ThemeColor, Color) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -120,6 +105,13 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ThemesSection(systemDarkTheme)
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ProfileImageSection()
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
@@ -145,11 +137,6 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ProfileImageSection()
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
@@ -169,8 +156,6 @@ fun PreviewAppearanceSettings() {
|
||||
languagePref = SharedPreference({ null }, {}),
|
||||
systemDarkTheme = SharedPreference({ null }, {}),
|
||||
changeIcon = {},
|
||||
showSettingsModal = { {} },
|
||||
editColor = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-7
@@ -4,7 +4,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
@@ -399,6 +399,18 @@ object ChatModel {
|
||||
currentUser.value = updated
|
||||
}
|
||||
|
||||
fun updateCurrentUserUiThemes(rhId: Long?, uiThemes: ThemeModeOverrides?) {
|
||||
val current = currentUser.value ?: return
|
||||
val updated = current.copy(
|
||||
uiThemes = uiThemes
|
||||
)
|
||||
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
users[i] = users[i].copy(user = updated)
|
||||
}
|
||||
currentUser.value = updated
|
||||
}
|
||||
|
||||
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -682,7 +694,8 @@ data class User(
|
||||
override val showNtfs: Boolean,
|
||||
val sendRcptsContacts: Boolean,
|
||||
val sendRcptsSmallGroups: Boolean,
|
||||
val viewPwdHash: UserPwdHash?
|
||||
val viewPwdHash: UserPwdHash?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): NamedChat, UserLike {
|
||||
override val displayName: String get() = profile.displayName
|
||||
override val fullName: String get() = profile.fullName
|
||||
@@ -709,6 +722,7 @@ data class User(
|
||||
sendRcptsContacts = true,
|
||||
sendRcptsSmallGroups = false,
|
||||
viewPwdHash = null,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1041,7 +1055,8 @@ data class Contact(
|
||||
override val updatedAt: Instant,
|
||||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean
|
||||
val contactGrpInvSent: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
override val id get() = "@$contactId"
|
||||
@@ -1113,7 +1128,8 @@ data class Contact(
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
contactGrpInvSent = false
|
||||
contactGrpInvSent = false,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1253,7 +1269,8 @@ data class GroupInfo (
|
||||
val chatSettings: ChatSettings,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant,
|
||||
val chatTs: Instant?
|
||||
val chatTs: Instant?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Group
|
||||
override val id get() = "#$groupId"
|
||||
@@ -1295,7 +1312,8 @@ data class GroupInfo (
|
||||
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now()
|
||||
chatTs = Clock.System.now(),
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1915,12 +1933,13 @@ data class ChatItem (
|
||||
itemDeleted: CIDeleted? = null,
|
||||
itemEdited: Boolean = false,
|
||||
itemTimed: CITimed? = null,
|
||||
itemLive: Boolean = false,
|
||||
deletable: Boolean = true,
|
||||
editable: Boolean = true
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
|
||||
meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
reactions = listOf(),
|
||||
@@ -2644,6 +2663,7 @@ data class CIFile(
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvAccepted -> false
|
||||
is CIFileStatus.RcvTransfer -> false
|
||||
is CIFileStatus.RcvAborted -> false
|
||||
is CIFileStatus.RcvCancelled -> false
|
||||
is CIFileStatus.RcvComplete -> true
|
||||
is CIFileStatus.RcvError -> false
|
||||
@@ -2665,6 +2685,7 @@ data class CIFile(
|
||||
is CIFileStatus.RcvInvitation -> null
|
||||
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
||||
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
||||
is CIFileStatus.RcvAborted -> null
|
||||
is CIFileStatus.RcvCancelled -> null
|
||||
is CIFileStatus.RcvComplete -> null
|
||||
is CIFileStatus.RcvError -> null
|
||||
@@ -2845,6 +2866,7 @@ sealed class CIFileStatus {
|
||||
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
|
||||
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||
@@ -2859,6 +2881,7 @@ sealed class CIFileStatus {
|
||||
is RcvInvitation -> false
|
||||
is RcvAccepted -> false
|
||||
is RcvTransfer -> false
|
||||
is RcvAborted -> false
|
||||
is RcvComplete -> false
|
||||
is RcvCancelled -> false
|
||||
is RcvError -> false
|
||||
|
||||
+132
-42
@@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.toggleShowMemberMessages
|
||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -25,8 +26,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.builtins.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.util.Date
|
||||
|
||||
@@ -106,6 +106,7 @@ class AppPreferences {
|
||||
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
|
||||
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -166,13 +167,20 @@ class AppPreferences {
|
||||
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
|
||||
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
|
||||
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name)
|
||||
val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = {
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME)
|
||||
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName)
|
||||
val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = {
|
||||
json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it)
|
||||
}, decode = {
|
||||
json.decodeFromString(MapSerializer(String.serializer(), String.serializer()), it)
|
||||
})
|
||||
// Deprecated. Remove key from preferences in 2025
|
||||
val themeOverridesOld = mkMapPreference(SHARED_PREFS_THEMES_OLD, mapOf(), encode = {
|
||||
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||
}, decode = {
|
||||
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||
jsonCoerceInputValues.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||
}, settingsThemes)
|
||||
val themeOverrides = mkThemeOverridesPreference()
|
||||
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
|
||||
|
||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||
@@ -267,6 +275,12 @@ class AppPreferences {
|
||||
set = fun(value) = prefs.putString(prefName, encode(value))
|
||||
)
|
||||
|
||||
private fun mkThemeOverridesPreference(): SharedPreference<List<ThemeOverrides>> =
|
||||
SharedPreference(
|
||||
get = fun() = themeOverridesStore ?: (readThemeOverrides()).also { themeOverridesStore = it },
|
||||
set = fun(value) { if (writeThemeOverrides(value)) { themeOverridesStore = value } }
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
|
||||
internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES"
|
||||
@@ -292,6 +306,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
|
||||
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
||||
@@ -342,8 +357,10 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds"
|
||||
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
|
||||
private const val SHARED_PREFS_THEMES = "Themes"
|
||||
private const val SHARED_PREFS_THEMES_OLD = "Themes"
|
||||
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
|
||||
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
|
||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||
@@ -358,6 +375,8 @@ class AppPreferences {
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
|
||||
|
||||
private var themeOverridesStore: List<ThemeOverrides>? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,8 +461,13 @@ object ChatController {
|
||||
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||
apiSetNetworkConfig(netCfg, ctrl)
|
||||
apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||
apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
||||
apiSetAppFilePaths(
|
||||
getMigrationTempFilesDirectory().absolutePath,
|
||||
getMigrationTempFilesDirectory().absolutePath,
|
||||
wallpapersDir.parentFile.absolutePath,
|
||||
remoteHostsDir.absolutePath,
|
||||
ctrl
|
||||
)
|
||||
apiStartChat(ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
@@ -662,22 +686,10 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) {
|
||||
val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl)
|
||||
suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) {
|
||||
val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl)
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set temp folder: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) {
|
||||
val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl)
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set files folder: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) {
|
||||
val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set remote hosts folder: ${r.responseType} ${r.details}")
|
||||
throw Exception("failed to set app file paths: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
||||
@@ -1163,6 +1175,20 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes))
|
||||
if (r is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiSetUserUIThemes bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes))
|
||||
if (r is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(rh: Long?): String? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
|
||||
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId))
|
||||
@@ -1337,9 +1363,9 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline))
|
||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
@@ -1358,7 +1384,23 @@ object ChatController {
|
||||
val maybeChatError = chatError(r)
|
||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
} else {
|
||||
} else if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||
Log.d(TAG, "apiReceiveFile FileNotApproved error")
|
||||
if (!auto) {
|
||||
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.file_not_approved_title),
|
||||
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
|
||||
confirmText = generalGetString(MR.strings.download_file),
|
||||
onConfirm = {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if (!auto) {
|
||||
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
|
||||
}
|
||||
}
|
||||
@@ -2216,9 +2258,14 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) {
|
||||
val encrypted = appPrefs.privacyEncryptLocalFiles.get()
|
||||
val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto)
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
val chatItem = apiReceiveFile(
|
||||
rhId,
|
||||
fileId,
|
||||
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
|
||||
auto = auto
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(rhId, user, chatItem)
|
||||
}
|
||||
@@ -2413,9 +2460,8 @@ sealed class CC {
|
||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||
class StartChat(val mainApp: Boolean): CC()
|
||||
class ApiStopChat: CC()
|
||||
class SetTempFolder(val tempFolder: String): CC()
|
||||
class SetFilesFolder(val filesFolder: String): CC()
|
||||
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
||||
@Serializable
|
||||
class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC()
|
||||
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||
@@ -2483,6 +2529,8 @@ sealed class CC {
|
||||
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||
class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC()
|
||||
class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC()
|
||||
class ApiCreateMyAddress(val userId: Long): CC()
|
||||
class ApiDeleteMyAddress(val userId: Long): CC()
|
||||
class ApiShowMyAddress(val userId: Long): CC()
|
||||
@@ -2501,7 +2549,7 @@ sealed class CC {
|
||||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC()
|
||||
class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
|
||||
class CancelFile(val fileId: Long): CC()
|
||||
// Remote control
|
||||
class SetLocalDeviceName(val displayName: String): CC()
|
||||
@@ -2550,9 +2598,7 @@ sealed class CC {
|
||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||
is StartChat -> "/_start main=${onOff(mainApp)}"
|
||||
is ApiStopChat -> "/_stop"
|
||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
||||
is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}"
|
||||
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||
@@ -2632,6 +2678,8 @@ sealed class CC {
|
||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||
is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}"
|
||||
is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}"
|
||||
is ApiCreateMyAddress -> "/_address $userId"
|
||||
is ApiDeleteMyAddress -> "/_delete_address $userId"
|
||||
is ApiShowMyAddress -> "/_show_address $userId"
|
||||
@@ -2652,6 +2700,7 @@ sealed class CC {
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile ->
|
||||
"/freceive $fileId" +
|
||||
(" approved_relays=${onOff(userApprovedRelays)}") +
|
||||
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
|
||||
(if (inline == null) "" else " inline=${onOff(inline)}")
|
||||
is CancelFile -> "/fcancel $fileId"
|
||||
@@ -2695,9 +2744,7 @@ sealed class CC {
|
||||
is ApiDeleteUser -> "apiDeleteUser"
|
||||
is StartChat -> "startChat"
|
||||
is ApiStopChat -> "apiStopChat"
|
||||
is SetTempFolder -> "setTempFolder"
|
||||
is SetFilesFolder -> "setFilesFolder"
|
||||
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
||||
is ApiSetAppFilePaths -> "apiSetAppFilePaths"
|
||||
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||
is ApiExportArchive -> "apiExportArchive"
|
||||
is ApiImportArchive -> "apiImportArchive"
|
||||
@@ -2765,6 +2812,8 @@ sealed class CC {
|
||||
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||
is ApiSetUserUIThemes -> "apiSetUserUIThemes"
|
||||
is ApiSetChatUIThemes -> "apiSetChatUIThemes"
|
||||
is ApiCreateMyAddress -> "apiCreateMyAddress"
|
||||
is ApiDeleteMyAddress -> "apiDeleteMyAddress"
|
||||
is ApiShowMyAddress -> "apiShowMyAddress"
|
||||
@@ -4036,6 +4085,15 @@ val json = Json {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
// Can decode unknown enum to default value specified for this field
|
||||
val jsonCoerceInputValues = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
val jsonShort = Json {
|
||||
prettyPrint = false
|
||||
ignoreUnknownKeys = true
|
||||
@@ -4892,13 +4950,14 @@ sealed class ChatErrorType {
|
||||
is FileCancel -> "fileCancel"
|
||||
is FileAlreadyExists -> "fileAlreadyExists"
|
||||
is FileRead -> "fileRead"
|
||||
is FileWrite -> "fileWrite"
|
||||
is FileWrite -> "fileWrite $message"
|
||||
is FileSend -> "fileSend"
|
||||
is FileRcvChunk -> "fileRcvChunk"
|
||||
is FileInternal -> "fileInternal"
|
||||
is FileImageType -> "fileImageType"
|
||||
is FileImageSize -> "fileImageSize"
|
||||
is FileNotReceived -> "fileNotReceived"
|
||||
is FileNotApproved -> "fileNotApproved"
|
||||
// is XFTPRcvFile -> "xftpRcvFile"
|
||||
// is XFTPSndFile -> "xftpSndFile"
|
||||
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
||||
@@ -4978,6 +5037,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType()
|
||||
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
|
||||
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
|
||||
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
|
||||
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
|
||||
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
|
||||
@@ -5476,6 +5536,7 @@ enum class NotificationsMode() {
|
||||
data class AppSettings(
|
||||
var networkConfig: NetCfg? = null,
|
||||
var privacyEncryptLocalFiles: Boolean? = null,
|
||||
var privacyAskToApproveRelays: Boolean? = null,
|
||||
var privacyAcceptImages: Boolean? = null,
|
||||
var privacyLinkPreviews: Boolean? = null,
|
||||
var privacyShowChatPreviews: Boolean? = null,
|
||||
@@ -5493,12 +5554,18 @@ data class AppSettings(
|
||||
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
|
||||
var iosCallKitEnabled: Boolean? = null,
|
||||
var iosCallKitCallsInRecents: Boolean? = null,
|
||||
var uiProfileImageCornerRadius: Float? = null,
|
||||
var uiColorScheme: String? = null,
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
var uiThemes: List<ThemeOverrides>? = null,
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
val def = defaults
|
||||
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
@@ -5516,6 +5583,11 @@ data class AppSettings(
|
||||
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||
if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius }
|
||||
if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme }
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||
return empty
|
||||
}
|
||||
|
||||
@@ -5530,6 +5602,7 @@ data class AppSettings(
|
||||
setNetCfg(net)
|
||||
}
|
||||
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
|
||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
||||
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||
@@ -5547,6 +5620,11 @@ data class AppSettings(
|
||||
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
|
||||
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
||||
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
||||
uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) }
|
||||
uiColorScheme?.let { def.currentTheme.set(it) }
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -5554,6 +5632,7 @@ data class AppSettings(
|
||||
get() = AppSettings(
|
||||
networkConfig = NetCfg.defaults,
|
||||
privacyEncryptLocalFiles = true,
|
||||
privacyAskToApproveRelays = true,
|
||||
privacyAcceptImages = true,
|
||||
privacyLinkPreviews = true,
|
||||
privacyShowChatPreviews = true,
|
||||
@@ -5570,7 +5649,12 @@ data class AppSettings(
|
||||
confirmDBUpgrades = false,
|
||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
|
||||
iosCallKitEnabled = true,
|
||||
iosCallKitCallsInRecents = false
|
||||
iosCallKitCallsInRecents = false,
|
||||
uiProfileImageCornerRadius = 22.5f,
|
||||
uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME,
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
uiThemes = null,
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
@@ -5579,6 +5663,7 @@ data class AppSettings(
|
||||
return defaults.copy(
|
||||
networkConfig = getNetCfg(),
|
||||
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
|
||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
||||
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||
@@ -5596,6 +5681,11 @@ data class AppSettings(
|
||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
|
||||
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
||||
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
||||
uiProfileImageCornerRadius = def.profileImageCornerRadius.get(),
|
||||
uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME,
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
uiThemes = def.themeOverrides.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,7 +2,6 @@ package com.sd.lib.compose.wheel_picker
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import chat.simplex.common.ui.theme.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
|
||||
/**
|
||||
* The default implementation of focus view in vertical.
|
||||
@@ -76,7 +76,7 @@ fun FWheelPickerFocusHorizontal(
|
||||
*/
|
||||
private val DefaultDividerColor: Color
|
||||
@Composable
|
||||
get() = (if (isSystemInDarkTheme()) {
|
||||
get() = (if (isInDarkTheme()) {
|
||||
Color.White
|
||||
} else {
|
||||
Color.Black
|
||||
|
||||
+6
@@ -42,6 +42,12 @@ fun runMigrations() {
|
||||
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
|
||||
}
|
||||
lastMigration.set(117)
|
||||
} else if (lastMigration.get() < 203) {
|
||||
// Moving to a different key for storing themes as a List
|
||||
val oldOverrides = ChatController.appPrefs.themeOverridesOld.get().values.toList()
|
||||
ChatController.appPrefs.themeOverrides.set(oldOverrides)
|
||||
ChatController.appPrefs.currentThemeIds.set(oldOverrides.associate { it.base.themeName to it.themeId })
|
||||
lastMigration.set(203)
|
||||
} else {
|
||||
lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE)
|
||||
break
|
||||
|
||||
+8
-7
@@ -43,14 +43,13 @@ val appPreferences: AppPreferences
|
||||
|
||||
val chatController: ChatController = ChatController
|
||||
|
||||
fun initChatControllerAndRunMigrations() {
|
||||
fun initChatControllerOnStart() {
|
||||
withLongRunningApi {
|
||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||
} else {
|
||||
initChatController()
|
||||
}
|
||||
runMigrations()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +87,13 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
return
|
||||
}
|
||||
platform.androidRestartNetworkObserver()
|
||||
controller.apiSetTempFolder(coreTmpDir.absolutePath)
|
||||
controller.apiSetFilesFolder(appFilesDir.absolutePath)
|
||||
if (appPlatform.isDesktop) {
|
||||
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
||||
}
|
||||
controller.apiSetAppFilePaths(
|
||||
appFilesDir.absolutePath,
|
||||
coreTmpDir.absolutePath,
|
||||
wallpapersDir.parentFile.absolutePath,
|
||||
remoteHostsDir.absolutePath,
|
||||
ctrl
|
||||
)
|
||||
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
|
||||
+56
-2
@@ -1,10 +1,12 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import com.charleskorn.kaml.*
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
@@ -14,8 +16,10 @@ expect val dataDir: File
|
||||
expect val tmpDir: File
|
||||
expect val filesDir: File
|
||||
expect val appFilesDir: File
|
||||
expect val wallpapersDir: File
|
||||
expect val coreTmpDir: File
|
||||
expect val dbAbsolutePrefixPath: String
|
||||
expect val preferencesDir: File
|
||||
|
||||
expect val chatDatabaseFileName: String
|
||||
expect val agentDatabaseFileName: String
|
||||
@@ -78,6 +82,20 @@ fun getAppFilePath(fileName: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun getWallpaperFilePath(fileName: String): String {
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
val s = File.separator
|
||||
val path = if (rh == null) {
|
||||
wallpapersDir.absolutePath + s + fileName
|
||||
} else {
|
||||
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName
|
||||
}
|
||||
File(path).parentFile.mkdirs()
|
||||
return path
|
||||
}
|
||||
|
||||
fun getPreferenceFilePath(fileName: String = "themes.yaml"): String = preferencesDir.absolutePath + File.separator + fileName
|
||||
|
||||
fun getLoadedFilePath(file: CIFile?): String? {
|
||||
val f = file?.fileSource?.filePath
|
||||
return if (f != null && file.loaded) {
|
||||
@@ -98,6 +116,42 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? {
|
||||
}
|
||||
}
|
||||
|
||||
fun readThemeOverrides(): List<ThemeOverrides> {
|
||||
return try {
|
||||
val file = File(getPreferenceFilePath("themes.yaml"))
|
||||
if (!file.exists()) return emptyList()
|
||||
|
||||
file.inputStream().use {
|
||||
val map = yaml.parseToYamlNode(it).yamlMap
|
||||
val list = map.get<YamlList>("themes")
|
||||
val res = ArrayList<ThemeOverrides>()
|
||||
list?.items?.forEach {
|
||||
try {
|
||||
res.add(yaml.decodeFromYamlNode(ThemeOverrides.serializer(), it))
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while reading specific theme: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
res.skipDuplicates()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while reading themes file: ${e.stackTraceToString()}")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun writeThemeOverrides(overrides: List<ThemeOverrides>): Boolean =
|
||||
try {
|
||||
File(getPreferenceFilePath("themes.yaml")).outputStream().use {
|
||||
val string = yaml.encodeToString(ThemesFile(themes = overrides))
|
||||
it.bufferedWriter().use { it.write(string) }
|
||||
}
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}")
|
||||
false
|
||||
}
|
||||
|
||||
private fun fileReady(file: CIFile, filePath: String) =
|
||||
File(filePath).exists() &&
|
||||
CIFile.cachedRemoteFileRequests[file.fileSource] != false
|
||||
|
||||
+4
@@ -1,11 +1,13 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
@@ -31,3 +33,5 @@ expect fun windowWidth(): Dp
|
||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||
|
||||
expect fun isRtl(text: CharSequence): Boolean
|
||||
|
||||
expect fun ImageResource.toComposeImageBitmap(): ImageBitmap?
|
||||
|
||||
+1
-12
@@ -31,17 +31,6 @@ val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 255)
|
||||
val FileDark = Color(101, 101, 106, 255)
|
||||
val SentMessageColor = Color(0x1E45B8FF)
|
||||
|
||||
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
|
||||
val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) {
|
||||
// Default color looks too light and better to have it here a little bit brighter
|
||||
if (alpha == SentMessageColor.alpha) {
|
||||
copy(min(SentMessageColor.alpha + 0.1f, 1f))
|
||||
} else {
|
||||
// Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant,
|
||||
// and it shouldn't be universal
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2
|
||||
|
||||
+537
-76
@@ -6,53 +6,136 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.platform.isInNightMode
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.serialization.Transient
|
||||
import java.util.UUID
|
||||
|
||||
enum class DefaultTheme {
|
||||
SYSTEM, LIGHT, DARK, SIMPLEX;
|
||||
LIGHT, DARK, SIMPLEX, BLACK;
|
||||
|
||||
companion object {
|
||||
const val SYSTEM_THEME_NAME: String = "SYSTEM"
|
||||
}
|
||||
|
||||
val themeName: String
|
||||
get() = name
|
||||
|
||||
val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK
|
||||
|
||||
// Call it only with base theme, not SYSTEM
|
||||
fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean {
|
||||
val palette = when (this) {
|
||||
SYSTEM -> return false
|
||||
LIGHT -> LightColorPalette
|
||||
DARK -> DarkColorPalette
|
||||
SIMPLEX -> SimplexColorPalette
|
||||
}
|
||||
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
|
||||
fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean {
|
||||
if (overrides == null) return false
|
||||
return overrides.colors != ThemeColors() ||
|
||||
overrides.wallpaper != null && (overrides.wallpaper.background != null || overrides.wallpaper.tint != null)
|
||||
}
|
||||
}
|
||||
|
||||
data class AppColors(
|
||||
val title: Color,
|
||||
val sentMessage: Color,
|
||||
val receivedMessage: Color
|
||||
)
|
||||
@Serializable
|
||||
enum class DefaultThemeMode {
|
||||
@SerialName("light") LIGHT,
|
||||
@SerialName("dark") DARK
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AppColors(
|
||||
title: Color,
|
||||
primaryVariant2: Color,
|
||||
sentMessage: Color,
|
||||
sentQuote: Color,
|
||||
receivedMessage: Color,
|
||||
receivedQuote: Color,
|
||||
) {
|
||||
var title by mutableStateOf(title, structuralEqualityPolicy())
|
||||
internal set
|
||||
var primaryVariant2 by mutableStateOf(primaryVariant2, structuralEqualityPolicy())
|
||||
internal set
|
||||
var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy())
|
||||
internal set
|
||||
var sentQuote by mutableStateOf(sentQuote, structuralEqualityPolicy())
|
||||
internal set
|
||||
var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy())
|
||||
internal set
|
||||
var receivedQuote by mutableStateOf(receivedQuote, structuralEqualityPolicy())
|
||||
internal set
|
||||
|
||||
fun copy(
|
||||
title: Color = this.title,
|
||||
primaryVariant2: Color = this.primaryVariant2,
|
||||
sentMessage: Color = this.sentMessage,
|
||||
sentQuote: Color = this.sentQuote,
|
||||
receivedMessage: Color = this.receivedMessage,
|
||||
receivedQuote: Color = this.receivedQuote,
|
||||
): AppColors = AppColors(
|
||||
title,
|
||||
primaryVariant2,
|
||||
sentMessage,
|
||||
sentQuote,
|
||||
receivedMessage,
|
||||
receivedQuote,
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
return buildString {
|
||||
append("AppColors(")
|
||||
append("title=$title, ")
|
||||
append("primaryVariant2=$primaryVariant2, ")
|
||||
append("sentMessage=$sentMessage, ")
|
||||
append("sentQuote=$sentQuote, ")
|
||||
append("receivedMessage=$receivedMessage, ")
|
||||
append("receivedQuote=$receivedQuote")
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AppWallpaper(
|
||||
background: Color? = null,
|
||||
tint: Color? = null,
|
||||
type: WallpaperType = WallpaperType.Empty,
|
||||
) {
|
||||
var background by mutableStateOf(background, structuralEqualityPolicy())
|
||||
internal set
|
||||
var tint by mutableStateOf(tint, structuralEqualityPolicy())
|
||||
internal set
|
||||
var type by mutableStateOf(type, structuralEqualityPolicy())
|
||||
internal set
|
||||
|
||||
fun copy(
|
||||
background: Color? = this.background,
|
||||
tint: Color? = this.tint,
|
||||
type: WallpaperType = this.type,
|
||||
): AppWallpaper = AppWallpaper(
|
||||
background,
|
||||
tint,
|
||||
type,
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
return buildString {
|
||||
append("AppWallpaper(")
|
||||
append("background=$background, ")
|
||||
append("tint=$tint, ")
|
||||
append("type=$type")
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ThemeColor {
|
||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE;
|
||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT;
|
||||
|
||||
fun fromColors(colors: Colors, appColors: AppColors): Color {
|
||||
fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? {
|
||||
return when (this) {
|
||||
PRIMARY -> colors.primary
|
||||
PRIMARY_VARIANT -> colors.primaryVariant
|
||||
@@ -61,8 +144,13 @@ enum class ThemeColor {
|
||||
BACKGROUND -> colors.background
|
||||
SURFACE -> colors.surface
|
||||
TITLE -> appColors.title
|
||||
PRIMARY_VARIANT2 -> appColors.primaryVariant2
|
||||
SENT_MESSAGE -> appColors.sentMessage
|
||||
SENT_QUOTE -> appColors.sentQuote
|
||||
RECEIVED_MESSAGE -> appColors.receivedMessage
|
||||
RECEIVED_QUOTE -> appColors.receivedQuote
|
||||
WALLPAPER_BACKGROUND -> appWallpaper.background
|
||||
WALLPAPER_TINT -> appWallpaper.tint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +163,13 @@ enum class ThemeColor {
|
||||
BACKGROUND -> generalGetString(MR.strings.color_background)
|
||||
SURFACE -> generalGetString(MR.strings.color_surface)
|
||||
TITLE -> generalGetString(MR.strings.color_title)
|
||||
PRIMARY_VARIANT2 -> generalGetString(MR.strings.color_primary_variant2)
|
||||
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
|
||||
SENT_QUOTE -> generalGetString(MR.strings.color_sent_quote)
|
||||
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
|
||||
RECEIVED_QUOTE -> generalGetString(MR.strings.color_received_quote)
|
||||
WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background)
|
||||
WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,45 +185,232 @@ data class ThemeColors(
|
||||
@SerialName("menus")
|
||||
val surface: String? = null,
|
||||
val title: String? = null,
|
||||
@SerialName("accentVariant2")
|
||||
val primaryVariant2: String? = null,
|
||||
val sentMessage: String? = null,
|
||||
@SerialName("sentReply")
|
||||
val sentQuote: String? = null,
|
||||
val receivedMessage: String? = null,
|
||||
@SerialName("receivedReply")
|
||||
val receivedQuote: String? = null,
|
||||
) {
|
||||
fun toColors(base: DefaultTheme): Colors {
|
||||
companion object {
|
||||
fun from(colors: Colors, appColors: AppColors): ThemeColors =
|
||||
ThemeColors(
|
||||
primary = colors.primary.toReadableHex(),
|
||||
primaryVariant = colors.primaryVariant.toReadableHex(),
|
||||
secondary = colors.secondary.toReadableHex(),
|
||||
secondaryVariant = colors.secondaryVariant.toReadableHex(),
|
||||
background = colors.background.toReadableHex(),
|
||||
surface = colors.surface.toReadableHex(),
|
||||
title = appColors.title.toReadableHex(),
|
||||
primaryVariant2 = appColors.primaryVariant2.toReadableHex(),
|
||||
sentMessage = appColors.sentMessage.toReadableHex(),
|
||||
sentQuote = appColors.sentQuote.toReadableHex(),
|
||||
receivedMessage = appColors.receivedMessage.toReadableHex(),
|
||||
receivedQuote = appColors.receivedQuote.toReadableHex(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeWallpaper (
|
||||
val preset: String? = null,
|
||||
val scale: Float? = null,
|
||||
val scaleType: WallpaperScaleType? = null,
|
||||
val background: String? = null,
|
||||
val tint: String? = null,
|
||||
val image: String? = null,
|
||||
val imageFile: String? = null,
|
||||
) {
|
||||
fun toAppWallpaper(): AppWallpaper {
|
||||
return AppWallpaper(
|
||||
background = background?.colorFromReadableHex(),
|
||||
tint = tint?.colorFromReadableHex(),
|
||||
type = WallpaperType.from(this) ?: WallpaperType.Empty
|
||||
)
|
||||
}
|
||||
|
||||
fun withFilledWallpaperBase64(): ThemeWallpaper {
|
||||
val aw = toAppWallpaper()
|
||||
val type = aw.type
|
||||
return ThemeWallpaper(
|
||||
image = if (type is WallpaperType.Image && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null,
|
||||
imageFile = null,
|
||||
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else 1f,
|
||||
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
|
||||
background = aw.background?.toReadableHex(),
|
||||
tint = aw.tint?.toReadableHex(),
|
||||
)
|
||||
}
|
||||
|
||||
fun withFilledWallpaperPath(): ThemeWallpaper {
|
||||
val aw = toAppWallpaper()
|
||||
val type = aw.type
|
||||
return ThemeWallpaper(
|
||||
image = null,
|
||||
imageFile = if (type is WallpaperType.Image) type.filename else null,
|
||||
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||
scale = if (scale == null) null else if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) scale else null,
|
||||
scaleType = if (scaleType == null) null else if (type is WallpaperType.Image) type.scaleType else null,
|
||||
background = aw.background?.toReadableHex(),
|
||||
tint = aw.tint?.toReadableHex(),
|
||||
)
|
||||
}
|
||||
|
||||
fun importFromString(): ThemeWallpaper =
|
||||
if (preset == null && image != null) {
|
||||
// Need to save image from string and to save its path
|
||||
try {
|
||||
val parsed = base64ToBitmap(image)
|
||||
val filename = saveWallpaperFile(parsed)
|
||||
copy(image = null, imageFile = filename)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while parsing/copying the image: ${e.stackTraceToString()}")
|
||||
ThemeWallpaper()
|
||||
}
|
||||
} else this
|
||||
|
||||
companion object {
|
||||
fun from(type: WallpaperType, background: String?, tint: String?): ThemeWallpaper {
|
||||
return ThemeWallpaper(
|
||||
image = null,
|
||||
imageFile = if (type is WallpaperType.Image) type.filename else null,
|
||||
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else null,
|
||||
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
|
||||
background = background,
|
||||
tint = tint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemesFile(
|
||||
val themes: List<ThemeOverrides> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ThemeOverrides (
|
||||
val themeId: String = UUID.randomUUID().toString(),
|
||||
val base: DefaultTheme,
|
||||
val colors: ThemeColors = ThemeColors(),
|
||||
val wallpaper: ThemeWallpaper? = null,
|
||||
) {
|
||||
|
||||
fun isSame(type: WallpaperType?, themeName: String): Boolean =
|
||||
(
|
||||
(wallpaper?.preset != null && type is WallpaperType.Preset && wallpaper.preset == type.filename) ||
|
||||
(wallpaper?.imageFile != null && type is WallpaperType.Image) ||
|
||||
(wallpaper?.preset == null && wallpaper?.imageFile == null && (type == WallpaperType.Empty || type == null))
|
||||
) && base.themeName == themeName
|
||||
|
||||
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.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
|
||||
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
||||
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
|
||||
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
||||
ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
|
||||
ThemeColor.WALLPAPER_TINT -> colors.copy()
|
||||
}, wallpaper = when (name) {
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
|
||||
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
|
||||
else -> wallpaper?.copy()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?): Colors {
|
||||
val baseColors = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPalette
|
||||
DefaultTheme.DARK -> DarkColorPalette
|
||||
DefaultTheme.SIMPLEX -> SimplexColorPalette
|
||||
// shouldn't be here
|
||||
DefaultTheme.SYSTEM -> LightColorPalette
|
||||
DefaultTheme.BLACK -> BlackColorPalette
|
||||
}
|
||||
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,
|
||||
primary = perChatTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: baseColors.primary,
|
||||
primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
|
||||
secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary,
|
||||
secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
|
||||
background = perChatTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: baseColors.background,
|
||||
surface = perChatTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: baseColors.surface,
|
||||
)
|
||||
}
|
||||
|
||||
fun toAppColors(base: DefaultTheme): AppColors {
|
||||
fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): AppColors {
|
||||
val baseColors = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPaletteApp
|
||||
DefaultTheme.DARK -> DarkColorPaletteApp
|
||||
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
|
||||
// shouldn't be here
|
||||
DefaultTheme.SYSTEM -> LightColorPaletteApp
|
||||
DefaultTheme.BLACK -> BlackColorPaletteApp
|
||||
}
|
||||
|
||||
val sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage
|
||||
val sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?: baseColors.sentQuote
|
||||
val receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage
|
||||
val receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?: baseColors.receivedQuote
|
||||
return baseColors.copy(
|
||||
title = title?.colorFromReadableHex() ?: baseColors.title,
|
||||
sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage,
|
||||
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
|
||||
title = perChatTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: baseColors.title,
|
||||
primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?: perUserTheme?.primaryVariant2?.colorFromReadableHex() ?: colors.primaryVariant2?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?: baseColors.primaryVariant2,
|
||||
sentMessage = if (perChatTheme?.sentMessage != null) perChatTheme.sentMessage.colorFromReadableHex()
|
||||
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentMessage?.colorFromReadableHex() ?: sentMessageFallback
|
||||
else sentMessageFallback,
|
||||
sentQuote = if (perChatTheme?.sentQuote != null) perChatTheme.sentQuote.colorFromReadableHex()
|
||||
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentQuote?.colorFromReadableHex() ?: sentQuoteFallback
|
||||
else sentQuoteFallback,
|
||||
receivedMessage = if (perChatTheme?.receivedMessage != null) perChatTheme.receivedMessage.colorFromReadableHex()
|
||||
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedMessage?.colorFromReadableHex() ?: receivedMessageFallback
|
||||
else receivedMessageFallback,
|
||||
receivedQuote = if (perChatTheme?.receivedQuote != null) perChatTheme.receivedQuote.colorFromReadableHex()
|
||||
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedQuote?.colorFromReadableHex() ?: receivedQuoteFallback
|
||||
else receivedQuoteFallback,
|
||||
)
|
||||
}
|
||||
|
||||
fun withFilledColors(base: DefaultTheme): ThemeColors {
|
||||
val c = toColors(base)
|
||||
val ac = toAppColors(base)
|
||||
fun toAppWallpaper(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverride?, materialBackgroundColor: Color): AppWallpaper {
|
||||
val mainType = when {
|
||||
themeOverridesForType != null -> themeOverridesForType
|
||||
// type can be null if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty.
|
||||
// It's not null to override upper level wallpaper
|
||||
perChatTheme?.wallpaper != null -> perChatTheme.wallpaper.toAppWallpaper().type
|
||||
perUserTheme?.wallpaper != null -> perUserTheme.wallpaper.toAppWallpaper().type
|
||||
else -> wallpaper?.toAppWallpaper()?.type ?: return AppWallpaper()
|
||||
}
|
||||
val first: ThemeWallpaper? = if (mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper()?.type)) perChatTheme?.wallpaper else null
|
||||
val second: ThemeWallpaper? = if (mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper()?.type)) perUserTheme?.wallpaper else null
|
||||
val third: ThemeWallpaper? = if (mainType.sameType(this.wallpaper?.toAppWallpaper()?.type)) this.wallpaper else null
|
||||
|
||||
return AppWallpaper(type = when (mainType) {
|
||||
is WallpaperType.Preset -> mainType.copy(
|
||||
scale = mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale
|
||||
)
|
||||
is WallpaperType.Image -> mainType.copy(
|
||||
scale = if (themeOverridesForType == null) mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale else second?.scale ?: third?.scale ?: mainType.scale,
|
||||
scaleType = if (themeOverridesForType == null) mainType.scaleType ?: first?.scaleType ?: second?.scaleType ?: third?.scaleType else second?.scaleType ?: third?.scaleType ?: mainType.scaleType,
|
||||
filename = if (themeOverridesForType == null) mainType.filename else first?.imageFile ?: second?.imageFile ?: third?.imageFile ?: mainType.filename,
|
||||
)
|
||||
is WallpaperType.Empty -> mainType
|
||||
},
|
||||
background = (first?.background ?: second?.background ?: third?.background)?.colorFromReadableHex() ?: mainType.defaultBackgroundColor(base, materialBackgroundColor),
|
||||
tint = (first?.tint ?: second?.tint ?: third?.tint)?.colorFromReadableHex() ?: mainType.defaultTintColor(base)
|
||||
)
|
||||
}
|
||||
|
||||
fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): ThemeColors {
|
||||
val c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme)
|
||||
val ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme)
|
||||
return ThemeColors(
|
||||
primary = c.primary.toReadableHex(),
|
||||
primaryVariant = c.primaryVariant.toReadableHex(),
|
||||
@@ -139,23 +419,71 @@ data class ThemeColors(
|
||||
background = c.background.toReadableHex(),
|
||||
surface = c.surface.toReadableHex(),
|
||||
title = ac.title.toReadableHex(),
|
||||
primaryVariant2 = ac.primaryVariant2.toReadableHex(),
|
||||
sentMessage = ac.sentMessage.toReadableHex(),
|
||||
receivedMessage = ac.receivedMessage.toReadableHex()
|
||||
sentQuote = ac.sentQuote.toReadableHex(),
|
||||
receivedMessage = ac.receivedMessage.toReadableHex(),
|
||||
receivedQuote = ac.receivedQuote.toReadableHex(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.colorFromReadableHex(): Color =
|
||||
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
|
||||
fun List<ThemeOverrides>.getTheme(themeId: String?): ThemeOverrides? =
|
||||
firstOrNull { it.themeId == themeId }
|
||||
|
||||
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
|
||||
fun List<ThemeOverrides>.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? =
|
||||
firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)}
|
||||
|
||||
fun List<ThemeOverrides>.replace(theme: ThemeOverrides): List<ThemeOverrides> {
|
||||
val index = indexOfFirst { it.themeId == theme.themeId ||
|
||||
// prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand)
|
||||
it.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName)
|
||||
}
|
||||
return if (index != -1) {
|
||||
val a = ArrayList(this)
|
||||
a[index] = theme
|
||||
a
|
||||
} else {
|
||||
this + theme
|
||||
}
|
||||
}
|
||||
|
||||
fun List<ThemeOverrides>.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) }
|
||||
|
||||
/** See [ThemesTest.testSkipDuplicates] */
|
||||
fun List<ThemeOverrides>.skipDuplicates(): List<ThemeOverrides> {
|
||||
val res = ArrayList<ThemeOverrides>()
|
||||
forEach { theme ->
|
||||
val themeType = WallpaperType.from(theme.wallpaper)
|
||||
if (res.none { it.themeId == theme.themeId || it.isSame(themeType, theme.base.themeName) }) {
|
||||
res.add(theme)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeOverrides (
|
||||
val base: DefaultTheme,
|
||||
val colors: ThemeColors
|
||||
data class ThemeModeOverrides (
|
||||
val light: ThemeModeOverride? = null,
|
||||
val dark: ThemeModeOverride? = null
|
||||
) {
|
||||
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides {
|
||||
fun preferredMode(darkTheme: Boolean): ThemeModeOverride? = when (darkTheme) {
|
||||
false -> light
|
||||
else -> dark
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ThemeModeOverride (
|
||||
val mode: DefaultThemeMode = CurrentColors.value.base.mode,
|
||||
val colors: ThemeColors = ThemeColors(),
|
||||
val wallpaper: ThemeWallpaper? = null,
|
||||
) {
|
||||
|
||||
@Transient
|
||||
val type = WallpaperType.from(wallpaper)
|
||||
|
||||
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride {
|
||||
return copy(colors = when (name) {
|
||||
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
||||
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
||||
@@ -164,9 +492,27 @@ data class ThemeOverrides (
|
||||
ThemeColor.BACKGROUND -> colors.copy(background = color)
|
||||
ThemeColor.SURFACE -> colors.copy(surface = color)
|
||||
ThemeColor.TITLE -> colors.copy(title = color)
|
||||
ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
|
||||
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
||||
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
|
||||
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
||||
})
|
||||
ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
|
||||
ThemeColor.WALLPAPER_TINT -> colors.copy()
|
||||
}, wallpaper = when (name) {
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
|
||||
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
|
||||
else -> wallpaper?.copy()
|
||||
}
|
||||
)
|
||||
}
|
||||
companion object {
|
||||
fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride =
|
||||
ThemeModeOverride(
|
||||
mode = mode,
|
||||
colors = ThemeOverrides(base = base).withFilledColors(base, null, null, null, null, null),
|
||||
wallpaper = ThemeWallpaper(preset = PresetWallpaper.SCHOOL.filename)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +550,6 @@ val DarkColorPalette = darkColors(
|
||||
// background = Color.Black,
|
||||
surface = Color(0xFF222222),
|
||||
// background = Color(0xFF121212),
|
||||
// surface = Color(0xFF121212),
|
||||
error = Color.Red,
|
||||
onBackground = Color(0xFFFFFBFA),
|
||||
onSurface = Color(0xFFFFFBFA),
|
||||
@@ -212,8 +557,11 @@ val DarkColorPalette = darkColors(
|
||||
)
|
||||
val DarkColorPaletteApp = AppColors(
|
||||
title = SimplexBlue,
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
primaryVariant2 = Color(0xFF18262E),
|
||||
sentMessage = Color(0xFF18262E),
|
||||
sentQuote = Color(0xFF1D3847),
|
||||
receivedMessage = Color(0xff262627),
|
||||
receivedQuote = Color(0xff373739),
|
||||
)
|
||||
|
||||
val LightColorPalette = lightColors(
|
||||
@@ -231,8 +579,11 @@ val LightColorPalette = lightColors(
|
||||
)
|
||||
val LightColorPaletteApp = AppColors(
|
||||
title = SimplexBlue,
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
primaryVariant2 = Color(0xFFE9F7FF),
|
||||
sentMessage = Color(0xFFE9F7FF),
|
||||
sentQuote = Color(0xFFD6F0FF),
|
||||
receivedMessage = Color(0xfff5f5f6),
|
||||
receivedQuote = Color(0xffececee),
|
||||
)
|
||||
|
||||
val SimplexColorPalette = darkColors(
|
||||
@@ -251,11 +602,39 @@ val SimplexColorPalette = darkColors(
|
||||
)
|
||||
val SimplexColorPaletteApp = AppColors(
|
||||
title = Color(0xFF267BE5),
|
||||
sentMessage = SentMessageColor,
|
||||
receivedMessage = Color(0x20B1B0B5)
|
||||
primaryVariant2 = Color(0xFF172941),
|
||||
sentMessage = Color(0xFF172941),
|
||||
sentQuote = Color(0xFF1C3A57),
|
||||
receivedMessage = Color(0xff25283a),
|
||||
receivedQuote = Color(0xff36394a),
|
||||
)
|
||||
|
||||
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
|
||||
val BlackColorPalette = darkColors(
|
||||
primary = Color(0xff0077e0), // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = Color(0xff0077e0),
|
||||
secondary = HighOrLowlight,
|
||||
secondaryVariant = DarkGray,
|
||||
background = Color(0xff070707),
|
||||
surface = Color(0xff161617),
|
||||
// background = Color(0xFF121212),
|
||||
// surface = Color(0xFF121212),
|
||||
error = Color.Red,
|
||||
onBackground = Color(0xFFFFFBFA),
|
||||
onSurface = Color(0xFFFFFBFA),
|
||||
// onError: Color = Color.Black,
|
||||
)
|
||||
val BlackColorPaletteApp = AppColors(
|
||||
title = Color(0xff0077e0),
|
||||
primaryVariant2 = Color(0xff243747),
|
||||
sentMessage = Color(0xFF18262E),
|
||||
sentQuote = Color(0xFF1D3847),
|
||||
receivedMessage = Color(0xff1b1b1b),
|
||||
receivedQuote = Color(0xff29292b),
|
||||
)
|
||||
|
||||
var systemInDarkThemeCurrently: Boolean = isInNightMode()
|
||||
|
||||
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()))
|
||||
|
||||
@Composable
|
||||
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
|
||||
@@ -263,31 +642,113 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi
|
||||
@Composable
|
||||
expect fun isSystemInDarkTheme(): Boolean
|
||||
|
||||
internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp }
|
||||
internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() }
|
||||
|
||||
val MaterialTheme.appColors: AppColors
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalAppColors.current
|
||||
|
||||
fun AppColors.updateColorsFrom(other: AppColors) {
|
||||
title = other.title
|
||||
primaryVariant2 = other.primaryVariant2
|
||||
sentMessage = other.sentMessage
|
||||
sentQuote = other.sentQuote
|
||||
receivedMessage = other.receivedMessage
|
||||
receivedQuote = other.receivedQuote
|
||||
}
|
||||
|
||||
fun AppWallpaper.updateWallpaperFrom(other: AppWallpaper) {
|
||||
background = other.background
|
||||
tint = other.tint
|
||||
type = other.type
|
||||
}
|
||||
|
||||
val MaterialTheme.wallpaper: AppWallpaper
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalAppWallpaper.current
|
||||
|
||||
fun reactOnDarkThemeChanges(isDark: Boolean) {
|
||||
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) {
|
||||
systemInDarkThemeCurrently = isDark
|
||||
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.value.colors.isLight == isDark) {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark)
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LaunchedEffect(darkTheme) {
|
||||
// For preview
|
||||
if (darkTheme != null)
|
||||
CurrentColors.value = ThemeManager.currentColors(darkTheme)
|
||||
}
|
||||
val systemDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(systemDark) {
|
||||
reactOnDarkThemeChanges(systemDark)
|
||||
// TODO: Fix preview working with dark/light theme
|
||||
|
||||
// LaunchedEffect(darkTheme) {
|
||||
// // For preview
|
||||
// if (darkTheme != null)
|
||||
// CurrentColors.value = ThemeManager.currentColors(darkTheme, null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
// }
|
||||
val systemDark = rememberUpdatedState(isSystemInDarkTheme())
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow vs LaunchedEffect reduce number of recomposes
|
||||
snapshotFlow { systemDark.value }
|
||||
.collect {
|
||||
reactOnDarkThemeChanges(systemDark.value)
|
||||
}
|
||||
}
|
||||
val theme by CurrentColors.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes
|
||||
snapshotFlow { chatModel.currentUser.value?.uiThemes }
|
||||
.collect {
|
||||
ThemeManager.applyTheme(appPrefs.currentTheme.get()!!)
|
||||
}
|
||||
}
|
||||
MaterialTheme(
|
||||
colors = theme.colors,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
|
||||
val rememberedAppColors = remember {
|
||||
// Explicitly creating a new object here so we don't mutate the initial [appColors]
|
||||
// provided, and overwrite the values set in it.
|
||||
theme.appColors.copy()
|
||||
}.apply { updateColorsFrom(theme.appColors) }
|
||||
val rememberedWallpaper = remember {
|
||||
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
|
||||
// provided, and overwrite the values set in it.
|
||||
theme.wallpaper.copy()
|
||||
}.apply { updateWallpaperFrom(theme.wallpaper) }
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||
LocalAppColors provides rememberedAppColors,
|
||||
LocalAppWallpaper provides rememberedWallpaper,
|
||||
content = content)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colors = theme.colors,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = {
|
||||
val rememberedAppColors = remember {
|
||||
// Explicitly creating a new object here so we don't mutate the initial [appColors]
|
||||
// provided, and overwrite the values set in it.
|
||||
theme.appColors.copy()
|
||||
}.apply { updateColorsFrom(theme.appColors) }
|
||||
val rememberedWallpaper = remember {
|
||||
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
|
||||
// provided, and overwrite the values set in it.
|
||||
theme.wallpaper.copy()
|
||||
}.apply { updateWallpaperFrom(theme.wallpaper) }
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||
LocalAppColors provides rememberedAppColors,
|
||||
LocalAppWallpaper provides rememberedWallpaper,
|
||||
content = content)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+184
-109
@@ -1,14 +1,14 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.model.AppPreferences
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.platform.platform
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import java.io.File
|
||||
|
||||
// https://github.com/rsms/inter
|
||||
// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt
|
||||
@@ -18,140 +18,215 @@ expect val EmojiFont: FontFamily
|
||||
object ThemeManager {
|
||||
private val appPrefs: AppPreferences = ChatController.appPrefs
|
||||
|
||||
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors)
|
||||
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper())
|
||||
|
||||
private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
|
||||
DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK
|
||||
DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
||||
DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK
|
||||
DefaultTheme.SIMPLEX.themeName -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
||||
DefaultTheme.BLACK.themeName -> BlackColorPalette to DefaultTheme.BLACK
|
||||
else -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
||||
}
|
||||
|
||||
fun currentColors(darkForSystemTheme: Boolean): ActiveTheme {
|
||||
private fun nonSystemThemeName(): String {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val themeOverrides = appPrefs.themeOverrides.get()
|
||||
|
||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
||||
return if (themeName != DefaultTheme.SYSTEM_THEME_NAME) {
|
||||
themeName
|
||||
} else {
|
||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
||||
if (systemInDarkThemeCurrently) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName
|
||||
}
|
||||
val theme = themeOverrides[nonSystemThemeName]
|
||||
}
|
||||
|
||||
fun defaultActiveTheme(appSettingsTheme: List<ThemeOverrides>): ThemeOverrides? {
|
||||
val nonSystemThemeName = nonSystemThemeName()
|
||||
val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
|
||||
return appSettingsTheme.getTheme(defaultThemeId)
|
||||
}
|
||||
|
||||
fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ThemeModeOverride {
|
||||
val perUserTheme = if (!CurrentColors.value.colors.isLight) perUserTheme?.dark else perUserTheme?.light
|
||||
if (perUserTheme != null) {
|
||||
return perUserTheme
|
||||
}
|
||||
val defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper)
|
||||
}
|
||||
|
||||
fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ActiveTheme {
|
||||
val themeName = appPrefs.currentTheme.get()!!
|
||||
val nonSystemThemeName = nonSystemThemeName()
|
||||
val defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
|
||||
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)
|
||||
DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
|
||||
DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.DARK)))
|
||||
DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.SIMPLEX)))
|
||||
DefaultTheme.BLACK.themeName -> ActiveTheme(DefaultTheme.BLACK.themeName, DefaultTheme.BLACK, BlackColorPalette, BlackColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.BLACK)))
|
||||
else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
|
||||
}
|
||||
if (theme == null) {
|
||||
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third)
|
||||
|
||||
val perUserTheme = if (baseTheme.colors.isLight) perUserTheme?.light else perUserTheme?.dark
|
||||
val theme = (appSettingsTheme.sameTheme(themeOverridesForType ?: perChatTheme?.type ?: perUserTheme?.type ?: defaultTheme?.wallpaper?.toAppWallpaper()?.type, nonSystemThemeName) ?: defaultTheme)
|
||||
|
||||
if (theme == null && perUserTheme == null && perChatTheme == null && themeOverridesForType == null) {
|
||||
return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper)
|
||||
}
|
||||
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
|
||||
val presetWallpaperTheme = when {
|
||||
perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PresetWallpaper.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||
perUserTheme?.wallpaper != null -> if (perUserTheme.wallpaper.preset != null) PresetWallpaper.from(perUserTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||
else -> if (theme?.wallpaper?.preset != null) PresetWallpaper.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||
}
|
||||
val themeOrEmpty = theme ?: ThemeOverrides(base = baseTheme.base)
|
||||
val colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
|
||||
return ActiveTheme(
|
||||
themeName,
|
||||
baseTheme.base,
|
||||
colors,
|
||||
themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
|
||||
themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
fun currentThemeOverridesForExport(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()
|
||||
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
||||
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base))
|
||||
fun currentThemeOverridesForExport(perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides {
|
||||
val current = currentColors(null, perChatTheme, perUserTheme, appPrefs.themeOverrides.get())
|
||||
val wType = current.wallpaper.type
|
||||
val wBackground = current.wallpaper.background
|
||||
val wTint = current.wallpaper.tint
|
||||
return ThemeOverrides(
|
||||
themeId = "",
|
||||
base = current.base,
|
||||
colors = ThemeColors.from(current.colors, current.appColors),
|
||||
wallpaper = if (wType !is WallpaperType.Empty) ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() else null
|
||||
)
|
||||
}
|
||||
|
||||
// colors, default theme enum, localized name of theme
|
||||
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
|
||||
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
|
||||
allThemes.add(
|
||||
Triple(
|
||||
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
|
||||
DefaultTheme.SYSTEM,
|
||||
generalGetString(MR.strings.theme_system)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
LightColorPalette,
|
||||
DefaultTheme.LIGHT,
|
||||
generalGetString(MR.strings.theme_light)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
DarkColorPalette,
|
||||
DefaultTheme.DARK,
|
||||
generalGetString(MR.strings.theme_dark)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
SimplexColorPalette,
|
||||
DefaultTheme.SIMPLEX,
|
||||
generalGetString(MR.strings.theme_simplex)
|
||||
)
|
||||
)
|
||||
return allThemes
|
||||
}
|
||||
|
||||
fun applyTheme(theme: String, darkForSystemTheme: Boolean) {
|
||||
fun applyTheme(theme: String) {
|
||||
appPrefs.currentTheme.set(theme)
|
||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
platform.androidSetNightModeIfSupported()
|
||||
}
|
||||
|
||||
fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) {
|
||||
fun changeDarkTheme(theme: String) {
|
||||
appPrefs.systemDarkTheme.set(theme)
|
||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
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
|
||||
fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||
val nonSystemThemeName = baseTheme.themeName
|
||||
val overrides = pref.get()
|
||||
val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
|
||||
val prevValue = overrides.getTheme(themeId) ?: ThemeOverrides(base = baseTheme)
|
||||
pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
|
||||
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
appPrefs.currentThemeIds.set(themeIds)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
||||
pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex())
|
||||
}
|
||||
|
||||
fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||
val nonSystemThemeName = baseTheme.themeName
|
||||
val overrides = pref.get()
|
||||
val theme = overrides.sameTheme(type, baseTheme.themeName)
|
||||
val prevValue = theme ?: ThemeOverrides(base = baseTheme)
|
||||
pref.set(overrides.replace(prevValue.copy(wallpaper = if (type != null && type !is WallpaperType.Empty) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else null)))
|
||||
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
appPrefs.currentThemeIds.set(themeIds)
|
||||
CurrentColors.value = currentColors( null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
fun copyFromSameThemeOverrides(type: WallpaperType?, lowerLevelOverride: ThemeModeOverride?, pref: MutableState<ThemeModeOverride>): Boolean {
|
||||
val overrides = appPrefs.themeOverrides.get()
|
||||
val sameWallpaper = if (lowerLevelOverride?.type?.sameType(type) == true) lowerLevelOverride.wallpaper else overrides.sameTheme(type, CurrentColors.value.base.themeName)?.wallpaper
|
||||
if (sameWallpaper == null) {
|
||||
if (type != null) {
|
||||
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null))
|
||||
} else {
|
||||
// Make an empty wallpaper to override any top level ones
|
||||
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper())
|
||||
}
|
||||
return true
|
||||
}
|
||||
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
|
||||
var type = sameWallpaper.toAppWallpaper().type
|
||||
if (type is WallpaperType.Image && sameWallpaper.imageFile == type.filename) {
|
||||
// same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override
|
||||
val filename = saveWallpaperFile(File(getWallpaperFilePath(type.filename)).toURI())
|
||||
if (filename != null) {
|
||||
type = WallpaperType.Image(filename, type.scale, type.scaleType)
|
||||
} else {
|
||||
Log.e(TAG, "Error while copying wallpaper from global overrides to chat overrides")
|
||||
return false
|
||||
}
|
||||
}
|
||||
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)
|
||||
val prevValue = pref.value
|
||||
pref.value = prevValue.copy(
|
||||
colors = ThemeColors(),
|
||||
wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
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 applyWallpaper(type: WallpaperType?, pref: MutableState<ThemeModeOverride>) {
|
||||
val prevValue = pref.value
|
||||
pref.value = prevValue.copy(
|
||||
wallpaper = if (type != null)
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
else null
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||
val wallpaper = theme.wallpaper?.importFromString()
|
||||
val nonSystemThemeName = theme.base.themeName
|
||||
val overrides = pref.get()
|
||||
val prevValue = overrides.getTheme(null, wallpaper?.toAppWallpaper()?.type, theme.base) ?: ThemeOverrides(base = theme.base)
|
||||
if (prevValue.wallpaper?.imageFile != null) {
|
||||
File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete()
|
||||
}
|
||||
pref.set(overrides.replace(prevValue.copy(base = theme.base, colors = theme.colors, wallpaper = wallpaper)))
|
||||
appPrefs.currentTheme.set(nonSystemThemeName)
|
||||
val currentThemeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||
currentThemeIds[nonSystemThemeName] = prevValue.themeId
|
||||
appPrefs.currentThemeIds.set(currentThemeIds)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
fun resetAllThemeColors(pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||
val nonSystemThemeName = nonSystemThemeName()
|
||||
val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] ?: return
|
||||
val overrides = pref.get()
|
||||
val prevValue = overrides.getTheme(themeId) ?: return
|
||||
pref.set(overrides.replace(prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))))
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
fun resetAllThemeColors(pref: MutableState<ThemeModeOverride>) {
|
||||
val prevValue = pref.value
|
||||
pref.value = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))
|
||||
}
|
||||
|
||||
fun removeTheme(themeId: String?) {
|
||||
val themes = ArrayList(appPrefs.themeOverrides.get())
|
||||
themes.removeAll { it.themeId == themeId }
|
||||
appPrefs.themeOverrides.set(themes)
|
||||
}
|
||||
|
||||
fun String.colorFromReadableHex(): Color =
|
||||
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
|
||||
|
||||
fun Color.toReadableHex(): String {
|
||||
val s = Integer.toHexString(toArgb())
|
||||
return when {
|
||||
this == Color.Transparent -> "#00ffffff"
|
||||
s.length == 1 -> "#ff$s$s$s$s$s$s"
|
||||
s.length == 2 -> "#ff$s$s$s"
|
||||
s.length == 3 -> "#ff$s$s"
|
||||
s.length == 6 && this.alpha == 0f -> "#00$s"
|
||||
s.length == 6 -> "#ff$s"
|
||||
else -> "#$s"
|
||||
}
|
||||
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 = "#" + Integer.toHexString(toArgb())
|
||||
|
||||
+65
@@ -29,6 +29,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -337,6 +338,16 @@ fun ChatInfoLayout(
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncContactConnection)
|
||||
}
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
// } else if (developerTools) {
|
||||
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||
// }
|
||||
@@ -642,6 +653,15 @@ private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, on
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WallpaperButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_image),
|
||||
stringResource(MR.strings.settings_section_title_chat_theme),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -675,6 +695,51 @@ fun ShareAddressButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.ChatWallpaperEditorModal(chat: Chat) {
|
||||
val themes = remember(CurrentColors.collectAsState().value.base) {
|
||||
(chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes
|
||||
?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes
|
||||
?: ThemeModeOverrides()
|
||||
}
|
||||
val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } }
|
||||
val initialTheme = remember(CurrentColors.collectAsState().value.base) {
|
||||
val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight)
|
||||
globalThemeUsed.value = preferred == null
|
||||
preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
ChatWallpaperEditor(
|
||||
initialTheme,
|
||||
applyToMode = if (themes.light == themes.dark) null else initialTheme.mode,
|
||||
globalThemeUsed = globalThemeUsed,
|
||||
save = { applyToMode, newTheme ->
|
||||
save(applyToMode, newTheme, chatModel.getChat(chat.id) ?: chat)
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) {
|
||||
val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides()
|
||||
val wallpaperFiles = listOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
||||
val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath())
|
||||
changedThemes = when (applyToMode) {
|
||||
null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK))
|
||||
DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode))
|
||||
DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode))
|
||||
}
|
||||
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null
|
||||
val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
|
||||
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ sealed class CIInfoTab {
|
||||
@Composable
|
||||
fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val appColors = CurrentColors.collectAsState().value.appColors
|
||||
val appColors = MaterialTheme.appColors
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
|
||||
|
||||
|
||||
+362
-349
@@ -5,25 +5,24 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
@@ -118,369 +117,373 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
val clipboard = LocalClipboardManager.current
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||
ChatLayout(
|
||||
chat,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Direct
|
||||
&& !chat.chatInfo.contact.ready
|
||||
&& chat.chatInfo.contact.active
|
||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||
val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
|
||||
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
|
||||
ChatLayout(
|
||||
chat,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Direct
|
||||
&& !chat.chatInfo.contact.ready
|
||||
&& chat.chatInfo.contact.active
|
||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
},
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
chatModel.chatId.value = null
|
||||
chatModel.groupMembers.clear()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
},
|
||||
attachmentOption,
|
||||
attachmentBottomSheetState,
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
chatModel.chatId.value = null
|
||||
chatModel.groupMembers.clear()
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
)
|
||||
deletedChatItem = r?.first
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh,
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
withBGApi {
|
||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
)
|
||||
deletedChatItem = r?.first
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
mode = mode
|
||||
)
|
||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId ->
|
||||
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId, onComplete ->
|
||||
withBGApi {
|
||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withBGApi {
|
||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withBGApi {
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withBGApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
findModelChat = { chatId ->
|
||||
chatModel.getChat(chatId)
|
||||
},
|
||||
findModelMember = { memberId ->
|
||||
chatModel.groupMembers.find { it.id == memberId }
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withBGApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
rh = chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
mode = mode
|
||||
itemId = cItem.id,
|
||||
add = add,
|
||||
reaction = reaction
|
||||
)
|
||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
suspend fun loadChatItemInfo(): ChatItemInfo? {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
}
|
||||
return ciInfo
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId ->
|
||||
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId, onComplete ->
|
||||
withBGApi {
|
||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withBGApi {
|
||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withBGApi {
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withBGApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
findModelChat = { chatId ->
|
||||
chatModel.getChat(chatId)
|
||||
},
|
||||
findModelMember = { memberId ->
|
||||
chatModel.groupMembers.find { it.id == memberId }
|
||||
},
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withBGApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
rh = chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = cItem.id,
|
||||
add = add,
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
suspend fun loadChatItemInfo(): ChatItemInfo? {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
}
|
||||
return ciInfo
|
||||
}
|
||||
withBGApi {
|
||||
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
}
|
||||
}) { close ->
|
||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (apiResp in controller.messagesChannel) {
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == chatRh &&
|
||||
msg is CR.ChatItemStatusUpdated &&
|
||||
msg.chatItem.chatItem.id == cItem.id
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
withBGApi {
|
||||
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
}
|
||||
}) { close ->
|
||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (apiResp in controller.messagesChannel) {
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == chatRh &&
|
||||
msg is CR.ChatItemStatusUpdated &&
|
||||
msg.chatItem.chatItem.id == cItem.id
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
withBGApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
)
|
||||
},
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
withBGApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
@@ -598,9 +601,19 @@ fun ChatLayout(
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
backgroundColor = Color.Unspecified
|
||||
) { contentPadding ->
|
||||
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||
val wallpaperType = MaterialTheme.wallpaper.type
|
||||
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
|
||||
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.then(if (wallpaperImage != null)
|
||||
Modifier.drawBehind { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
|
||||
else
|
||||
Modifier)
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ChatItemsList(
|
||||
@@ -1262,7 +1275,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image)
|
||||
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ComposeContextInvitingContactMemberView() {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent
|
||||
|
||||
@Composable
|
||||
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
|
||||
+3
-3
@@ -776,7 +776,7 @@ fun ComposeView(
|
||||
|
||||
@Composable
|
||||
fun MsgNotAllowedView(reason: String, icon: Painter) {
|
||||
val color = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val color = MaterialTheme.appColors.receivedMessage
|
||||
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, null, tint = MaterialTheme.colors.secondary)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
@@ -862,7 +862,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
||||
@@ -974,7 +974,7 @@ fun ComposeView(
|
||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||
val sendButtonColor =
|
||||
if (chat.chatInfo.incognito)
|
||||
if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
|
||||
if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
|
||||
else MaterialTheme.colors.primary
|
||||
SendMsgView(
|
||||
composeState,
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ fun ComposeVoiceView(
|
||||
) {
|
||||
val progress = rememberSaveable { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Box {
|
||||
Box(
|
||||
Modifier
|
||||
|
||||
+2
-2
@@ -30,8 +30,8 @@ fun ContextItemView(
|
||||
cancelContextItem: () -> Unit
|
||||
) {
|
||||
val sent = contextItem.chatDir.sent
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
|
||||
@Composable
|
||||
fun MessageText(attachment: ImageResource?, lines: Int) {
|
||||
|
||||
+10
-1
@@ -9,7 +9,6 @@ import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.*
|
||||
@@ -233,6 +232,16 @@ fun GroupChatInfoLayout(
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
+6
-9
@@ -123,13 +123,12 @@ private fun GroupPreferencesLayout(
|
||||
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
|
||||
}
|
||||
|
||||
// TODO enable simplexLinks preference in 5.8
|
||||
// SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
|
||||
// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
|
||||
// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
|
||||
// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
|
||||
// }
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
|
||||
val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
|
||||
FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
|
||||
applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
|
||||
}
|
||||
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
|
||||
@@ -189,8 +188,6 @@ private fun FeatureSection(
|
||||
generalGetString(MR.strings.feature_enabled_for),
|
||||
featureRoles,
|
||||
enableForRole,
|
||||
// remove in v5.8
|
||||
enabled = remember { mutableStateOf(false) },
|
||||
onSelected = { value ->
|
||||
onSelected(enableFeature.value, value)
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
@@ -64,7 +63,7 @@ fun CIFileView(
|
||||
fun fileAction() {
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation -> {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
|
||||
if (fileSizeValid(file)) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
@@ -176,6 +175,8 @@ fun CIFileView(
|
||||
} else {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvAborted ->
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary)
|
||||
is CIFileStatus.RcvComplete -> fileIcon()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
|
||||
+5
-4
@@ -83,8 +83,8 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
modifier = if (action && !inProgress.value) Modifier.clickable(onClick = {
|
||||
inProgress.value = true
|
||||
@@ -110,6 +110,7 @@ fun CIGroupInvitationView(
|
||||
.padding(bottom = 4.dp),
|
||||
) {
|
||||
groupInfoView()
|
||||
val secondaryColor = MaterialTheme.colors.secondary
|
||||
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
|
||||
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
|
||||
if (action) {
|
||||
@@ -117,7 +118,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
},
|
||||
color = if (inProgress.value)
|
||||
MaterialTheme.colors.secondary
|
||||
@@ -128,7 +129,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(groupInvitationStr())
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+3
-7
@@ -21,17 +21,12 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun CIImageView(
|
||||
image: String,
|
||||
file: CIFile?,
|
||||
metaColor: Color,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
@@ -51,7 +46,7 @@ fun CIImageView(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = metaColor
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,6 +73,7 @@ fun CIImageView(
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||
@@ -206,7 +202,7 @@ fun CIImageView(
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -12,7 +12,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.res.MR
|
||||
@@ -86,7 +85,7 @@ private fun CIMetaText(
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = CurrentColors.value.colors.secondary)
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (showStatus) {
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
@@ -115,6 +114,7 @@ fun reserveSpaceForMeta(
|
||||
meta: CIMeta,
|
||||
chatTTL: Int?,
|
||||
encrypted: Boolean?,
|
||||
secondaryColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean = false
|
||||
@@ -132,7 +132,7 @@ fun reserveSpaceForMeta(
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
res += iconSpace
|
||||
}
|
||||
if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) {
|
||||
if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
|
||||
res += iconSpace
|
||||
}
|
||||
if (encrypted != null) {
|
||||
|
||||
+7
-4
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.appColors
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
@@ -137,7 +138,7 @@ fun DecryptionErrorItemFixButton(
|
||||
onClick: () -> Unit,
|
||||
syncSupported: Boolean
|
||||
) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
@@ -164,10 +165,11 @@ fun DecryptionErrorItemFixButton(
|
||||
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(2.dp))
|
||||
val secondaryColor = MaterialTheme.colors.secondary
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.fix_connection))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
||||
},
|
||||
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
@@ -184,7 +186,7 @@ fun DecryptionErrorItem(
|
||||
ci: ChatItem,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
@@ -195,10 +197,11 @@ fun DecryptionErrorItem(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colors.secondary
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
|
||||
+3
-2
@@ -73,7 +73,7 @@ fun CIVideoView(
|
||||
VideoPreviewImageView(preview, onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
@@ -102,7 +102,7 @@ fun CIVideoView(
|
||||
if (file != null) {
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
}
|
||||
}
|
||||
@@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) {
|
||||
} else {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||
|
||||
+10
-6
@@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
|
||||
@@ -153,8 +154,8 @@ private fun VoiceLayout(
|
||||
}
|
||||
when {
|
||||
hasText -> {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Spacer(Modifier.width(6.dp))
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -220,10 +221,11 @@ private fun PlayPauseButton(
|
||||
error: Boolean,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit
|
||||
longClick: () -> Unit,
|
||||
icon: ImageResource = MR.images.ic_play_arrow_filled,
|
||||
) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
@@ -241,7 +243,7 @@ private fun PlayPauseButton(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon),
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp),
|
||||
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
@@ -294,6 +296,8 @@ private fun VoiceMsgIndicator(
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
|
||||
} else {
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
||||
}
|
||||
|
||||
+32
-34
@@ -819,40 +819,38 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemView() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
rhId = null,
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false
|
||||
)
|
||||
}
|
||||
fun PreviewChatItemView(
|
||||
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
|
||||
) {
|
||||
ChatItemView(
|
||||
rhId = null,
|
||||
ChatInfo.Direct.sampleData,
|
||||
chatItem,
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
syncMemberConnection = { _, _ -> },
|
||||
findModelChat = { null },
|
||||
findModelMember = { null },
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ import chat.simplex.common.ui.theme.*
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
|
||||
+16
-12
@@ -46,9 +46,6 @@ 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 ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
||||
MarkdownText(
|
||||
@@ -89,11 +86,11 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentQuote
|
||||
val receivedColor = MaterialTheme.appColors.receivedQuote
|
||||
Row(
|
||||
Modifier
|
||||
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
|
||||
.background(if (sent) sentColor else receivedColor)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
@@ -122,11 +119,11 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun ciQuoteView(qi: CIQuote) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentQuote
|
||||
val receivedColor = MaterialTheme.appColors.receivedQuote
|
||||
Row(
|
||||
Modifier
|
||||
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
|
||||
.background(if (sent) sentColor else receivedColor)
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
@@ -188,10 +185,17 @@ 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 && ci.meta.itemForwarded == null
|
||||
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
sent -> MaterialTheme.colors.background
|
||||
else -> MaterialTheme.colors.background
|
||||
}
|
||||
)
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
@@ -241,7 +245,7 @@ fun FramedItemView(
|
||||
} else {
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
|
||||
+2
-3
@@ -17,8 +17,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.MsgErrorType
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
@@ -51,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
||||
|
||||
@Composable
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ fun MarkdownText (
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL, null, showViaProxy = showViaProxy)
|
||||
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
|
||||
+2
@@ -486,6 +486,8 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
tmpDir.deleteRecursively()
|
||||
getMigrationTempFilesDirectory().deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
wallpapersDir.deleteRecursively()
|
||||
wallpapersDir.mkdirs()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
controller.appPrefs.storeDBPassphrase.set(true)
|
||||
controller.ctrl = null
|
||||
|
||||
+7
-1
@@ -2,12 +2,14 @@ package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Size
|
||||
@@ -51,7 +53,8 @@ fun ProfileImage(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
icon: ImageResource = MR.images.ic_account_circle_filled,
|
||||
color: Color = MaterialTheme.colors.secondaryVariant
|
||||
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||
backgroundColor: Color? = null
|
||||
) {
|
||||
Box(Modifier.size(size)) {
|
||||
if (image == null) {
|
||||
@@ -61,6 +64,9 @@ fun ProfileImage(
|
||||
else -> null
|
||||
}
|
||||
if (iconToReplace != null) {
|
||||
if (backgroundColor != null) {
|
||||
Box(Modifier.size(size * 0.7f).align(Alignment.Center).background(backgroundColor, CircleShape))
|
||||
}
|
||||
Icon(
|
||||
iconToReplace,
|
||||
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
||||
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
import kotlin.math.*
|
||||
|
||||
enum class PresetWallpaper(
|
||||
val res: ImageResource,
|
||||
val filename: String,
|
||||
val text: StringResource,
|
||||
val scale: Float,
|
||||
val background: Map<DefaultTheme, Color>,
|
||||
val tint: Map<DefaultTheme, Color>,
|
||||
val colors: Map<DefaultTheme, ThemeColors>,
|
||||
) {
|
||||
CATS(MR.images.wallpaper_cats, "cats", MR.strings.wallpaper_cats, 0.63f,
|
||||
wallpaperBackgrounds(light = "#ffF8F6EA"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#ffefdca6".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff4b3b0e".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff51400f".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff4b3b0e".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#fffffaed",
|
||||
sentQuote = "#fffaf0d6",
|
||||
receivedMessage = "#ffF8F7F4",
|
||||
receivedQuote = "#ffefede9",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff2f2919",
|
||||
sentQuote = "#ff473a1d",
|
||||
receivedMessage = "#ff272624",
|
||||
receivedQuote = "#ff373633",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff41371b",
|
||||
sentQuote = "#ff654f1c",
|
||||
receivedMessage = "#ff272624",
|
||||
receivedQuote = "#ff373633",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff41371b",
|
||||
sentQuote = "#ff654f1c",
|
||||
receivedMessage = "#ff1f1e1b",
|
||||
receivedQuote = "#ff2f2d27",
|
||||
),
|
||||
)
|
||||
),
|
||||
FLOWERS(MR.images.wallpaper_flowers, "flowers", MR.strings.wallpaper_flowers, 0.53f,
|
||||
wallpaperBackgrounds(light = "#ffE2FFE4"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#ff9CEA59".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff31560D".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff36600f".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff31560D".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#fff1ffe5",
|
||||
sentQuote = "#ffdcf9c4",
|
||||
receivedMessage = "#ffF4F8F2",
|
||||
receivedQuote = "#ffe7ece7",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff163521",
|
||||
sentQuote = "#ff1B5330",
|
||||
receivedMessage = "#ff242523",
|
||||
receivedQuote = "#ff353733",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff184739",
|
||||
sentQuote = "#ff1F6F4B",
|
||||
receivedMessage = "#ff242523",
|
||||
receivedQuote = "#ff353733",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff184739",
|
||||
sentQuote = "#ff1F6F4B",
|
||||
receivedMessage = "#ff1c1f1a",
|
||||
receivedQuote = "#ff282b25",
|
||||
),
|
||||
)
|
||||
),
|
||||
HEARTS(MR.images.wallpaper_hearts, "hearts", MR.strings.wallpaper_hearts, 0.59f,
|
||||
wallpaperBackgrounds(light = "#ffFDECEC"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#fffde0e0".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff3c0f0f".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff411010".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff3C0F0F".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#fffff4f4",
|
||||
sentQuote = "#ffffdfdf",
|
||||
receivedMessage = "#fff8f6f6",
|
||||
receivedQuote = "#ffefebeb",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff301515",
|
||||
sentQuote = "#ff4C1818",
|
||||
receivedMessage = "#ff242121",
|
||||
receivedQuote = "#ff3b3535",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff491A28",
|
||||
sentQuote = "#ff761F29",
|
||||
receivedMessage = "#ff242121",
|
||||
receivedQuote = "#ff3b3535",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff491A28",
|
||||
sentQuote = "#ff761F29",
|
||||
receivedMessage = "#ff1f1b1b",
|
||||
receivedQuote = "#ff2e2626",
|
||||
),
|
||||
)
|
||||
),
|
||||
KIDS(MR.images.wallpaper_kids, "kids", MR.strings.wallpaper_kids, 0.53f,
|
||||
wallpaperBackgrounds(light = "#ffdbfdfb"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#ffadeffc".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff16404B".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff184753".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff16404B".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#ffeafeff",
|
||||
sentQuote = "#ffcbf4f7",
|
||||
receivedMessage = "#fff3fafa",
|
||||
receivedQuote = "#ffe4efef",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff16302F",
|
||||
sentQuote = "#ff1a4a49",
|
||||
receivedMessage = "#ff252626",
|
||||
receivedQuote = "#ff373A39",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff1a4745",
|
||||
sentQuote = "#ff1d6b69",
|
||||
receivedMessage = "#ff252626",
|
||||
receivedQuote = "#ff373a39",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff1a4745",
|
||||
sentQuote = "#ff1d6b69",
|
||||
receivedMessage = "#ff1e1f1f",
|
||||
receivedQuote = "#ff262b29",
|
||||
),
|
||||
)
|
||||
),
|
||||
SCHOOL(MR.images.wallpaper_school, "school", MR.strings.wallpaper_school, 0.53f,
|
||||
wallpaperBackgrounds(light = "#ffE7F5FF"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#ffCEEBFF".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff0F293B".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff112f43".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff0F293B".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#ffeef9ff",
|
||||
sentQuote = "#ffD6EDFA",
|
||||
receivedMessage = "#ffF3F5F9",
|
||||
receivedQuote = "#ffe4e8ee",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff172833",
|
||||
sentQuote = "#ff1C3E4F",
|
||||
receivedMessage = "#ff26282c",
|
||||
receivedQuote = "#ff393c40",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff1A3C5D",
|
||||
sentQuote = "#ff235b80",
|
||||
receivedMessage = "#ff26282c",
|
||||
receivedQuote = "#ff393c40",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff1A3C5D",
|
||||
sentQuote = "#ff235b80",
|
||||
receivedMessage = "#ff1d1e22",
|
||||
receivedQuote = "#ff292b2f",
|
||||
),
|
||||
)
|
||||
),
|
||||
TRAVEL(MR.images.wallpaper_travel, "travel", MR.strings.wallpaper_travel, 0.68f,
|
||||
wallpaperBackgrounds(light = "#fff9eeff"),
|
||||
tint = mapOf(
|
||||
DefaultTheme.LIGHT to "#ffeedbfe".colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff311E48".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff35204e".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff311E48".colorFromReadableHex()
|
||||
),
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to ThemeColors(
|
||||
sentMessage = "#fffcf6ff",
|
||||
sentQuote = "#fff2e0fc",
|
||||
receivedMessage = "#ffF6F4F7",
|
||||
receivedQuote = "#ffede9ee",
|
||||
),
|
||||
DefaultTheme.DARK to ThemeColors(
|
||||
sentMessage = "#ff33263B",
|
||||
sentQuote = "#ff53385E",
|
||||
receivedMessage = "#ff272528",
|
||||
receivedQuote = "#ff3B373E",
|
||||
),
|
||||
DefaultTheme.SIMPLEX to ThemeColors(
|
||||
sentMessage = "#ff3C255D",
|
||||
sentQuote = "#ff623485",
|
||||
receivedMessage = "#ff26273B",
|
||||
receivedQuote = "#ff3A394F",
|
||||
),
|
||||
DefaultTheme.BLACK to ThemeColors(
|
||||
sentMessage = "#ff3C255D",
|
||||
sentQuote = "#ff623485",
|
||||
receivedMessage = "#ff231f23",
|
||||
receivedQuote = "#ff2c2931",
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
fun toType(base: DefaultTheme, scale: Float? = null): WallpaperType =
|
||||
WallpaperType.Preset(
|
||||
filename,
|
||||
scale ?: appPrefs.themeOverrides.get().firstOrNull { it.wallpaper != null && it.wallpaper.preset == filename && it.base == base }?.wallpaper?.scale ?: 1f
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun from(filename: String): PresetWallpaper? =
|
||||
entries.firstOrNull { it.filename == filename }
|
||||
}
|
||||
}
|
||||
|
||||
fun wallpaperBackgrounds(light: String): Map<DefaultTheme, Color> =
|
||||
mapOf(
|
||||
DefaultTheme.LIGHT to light.colorFromReadableHex(),
|
||||
DefaultTheme.DARK to "#ff121212".colorFromReadableHex(),
|
||||
DefaultTheme.SIMPLEX to "#ff111528".colorFromReadableHex(),
|
||||
DefaultTheme.BLACK to "#ff070707".colorFromReadableHex()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class WallpaperScaleType(val contentScale: ContentScale, val text: StringResource) {
|
||||
@SerialName("fill") FILL(ContentScale.Crop, MR.strings.wallpaper_scale_fill),
|
||||
@SerialName("fit") FIT(ContentScale.Fit, MR.strings.wallpaper_scale_fit),
|
||||
@SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.wallpaper_scale_repeat),
|
||||
}
|
||||
|
||||
sealed class WallpaperType {
|
||||
abstract val scale: Float?
|
||||
|
||||
val image by lazy {
|
||||
val filename = when (this) {
|
||||
is Preset -> filename
|
||||
is Image -> filename
|
||||
else -> return@lazy null
|
||||
}
|
||||
if (filename == "") return@lazy null
|
||||
if (cachedImages[filename] != null) {
|
||||
cachedImages[filename]
|
||||
} else {
|
||||
val res = if (this is Preset) {
|
||||
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).res.toComposeImageBitmap()!!
|
||||
} else {
|
||||
try {
|
||||
// In case of unintentional image deletion don't crash the app
|
||||
File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while loading wallpaper file: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
res?.prepareToDraw()
|
||||
cachedImages[filename] = res ?: return@lazy null
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fun sameType(other: WallpaperType?): Boolean =
|
||||
if (this is Preset && other is Preset) this.filename == other.filename
|
||||
else this.javaClass == other?.javaClass
|
||||
|
||||
fun samePreset(other: PresetWallpaper?): Boolean = this is Preset && filename == other?.filename
|
||||
|
||||
data class Preset(
|
||||
val filename: String,
|
||||
override val scale: Float?,
|
||||
): WallpaperType() {
|
||||
val predefinedImageScale = PresetWallpaper.from(filename)?.scale ?: 1f
|
||||
}
|
||||
|
||||
data class Image(
|
||||
val filename: String,
|
||||
override val scale: Float?,
|
||||
val scaleType: WallpaperScaleType?,
|
||||
): WallpaperType()
|
||||
|
||||
object Empty: WallpaperType() {
|
||||
override val scale: Float?
|
||||
get() = null
|
||||
}
|
||||
|
||||
fun defaultBackgroundColor(theme: DefaultTheme, materialBackground: Color): Color =
|
||||
if (this is Preset) {
|
||||
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).background[theme]!!
|
||||
} else {
|
||||
materialBackground
|
||||
}
|
||||
|
||||
fun defaultTintColor(theme: DefaultTheme): Color =
|
||||
if (this is Preset) {
|
||||
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).tint[theme]!!
|
||||
} else if (this is Image && scaleType == WallpaperScaleType.REPEAT) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
companion object {
|
||||
var cachedImages: MutableMap<String, ImageBitmap> = mutableMapOf()
|
||||
|
||||
fun from(wallpaper: ThemeWallpaper?): WallpaperType? {
|
||||
return if (wallpaper == null) {
|
||||
null
|
||||
} else if (wallpaper.preset != null) {
|
||||
Preset(wallpaper.preset, wallpaper.scale)
|
||||
} else if (wallpaper.imageFile != null) {
|
||||
Image(wallpaper.imageFile, wallpaper.scale, wallpaper.scaleType)
|
||||
} else {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color) = clipRect {
|
||||
val quality = FilterQuality.High
|
||||
fun repeat(imageScale: Float) {
|
||||
val scale = imageScale * density
|
||||
for (h in 0..(size.height / image.height / scale).roundToInt()) {
|
||||
for (w in 0..(size.width / image.width / scale).roundToInt()) {
|
||||
drawImage(
|
||||
image,
|
||||
dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()),
|
||||
dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()),
|
||||
colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn),
|
||||
filterQuality = quality
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawRect(background)
|
||||
when (imageType) {
|
||||
is WallpaperType.Preset -> repeat((imageType.scale ?: 1f) * imageType.predefinedImageScale)
|
||||
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
||||
WallpaperScaleType.REPEAT -> repeat(imageType.scale ?: 1f)
|
||||
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
||||
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
if (scaleType == WallpaperScaleType.FIT) {
|
||||
if (scaledWidth < size.width) {
|
||||
// has black lines at left and right sides
|
||||
var x = (size.width - scaledWidth) / 2
|
||||
while (x > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x -= scaledWidth
|
||||
}
|
||||
x = size.width - (size.width - scaledWidth) / 2
|
||||
while (x < size.width) {
|
||||
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x += scaledWidth
|
||||
}
|
||||
} else {
|
||||
// has black lines at top and bottom sides
|
||||
var y = (size.height - scaledHeight) / 2
|
||||
while (y > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y -= scaledHeight
|
||||
}
|
||||
y = size.height - (size.height - scaledHeight) / 2
|
||||
while (y < size.height) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y += scaledHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
drawRect(tint)
|
||||
}
|
||||
}
|
||||
is WallpaperType.Empty -> {}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -51,7 +51,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val titleColor = CurrentColors.collectAsState().value.appColors.title
|
||||
val titleColor = MaterialTheme.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
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
|
||||
|
||||
@Composable
|
||||
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
+4
-1
@@ -41,9 +41,12 @@ enum class ModalPlacement {
|
||||
}
|
||||
|
||||
class ModalData {
|
||||
private val state = mutableMapOf<String, MutableState<Any>>()
|
||||
private val state = mutableMapOf<String, MutableState<Any?>>()
|
||||
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
||||
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
||||
|
||||
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
|
||||
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector
|
||||
import chat.simplex.common.views.usersettings.AppearanceScope.editColor
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun ModalData.UserWallpaperEditor(
|
||||
theme: ThemeModeOverride,
|
||||
applyToMode: DefaultThemeMode?,
|
||||
globalThemeUsed: MutableState<Boolean>,
|
||||
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.settings_section_title_user_theme))
|
||||
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||
val wallpaperType = MaterialTheme.wallpaper.type
|
||||
|
||||
val onTypeCopyFromSameTheme = { type: WallpaperType? ->
|
||||
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
|
||||
false
|
||||
} else {
|
||||
ThemeManager.copyFromSameThemeOverrides(type, null, themeModeOverride)
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
globalThemeUsed.value = false
|
||||
true
|
||||
}
|
||||
}
|
||||
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
|
||||
if (globalThemeUsed.value) {
|
||||
onTypeCopyFromSameTheme(type)
|
||||
}
|
||||
}
|
||||
val onTypeChange: (WallpaperType?) -> Unit = { type: WallpaperType? ->
|
||||
if (globalThemeUsed.value) {
|
||||
preApplyGlobalIfNeeded(type)
|
||||
// Saves copied static image instead of original from global theme
|
||||
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
|
||||
} else {
|
||||
ThemeManager.applyWallpaper(type, themeModeOverride)
|
||||
}
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
}
|
||||
|
||||
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
val filename = saveWallpaperFile(to)
|
||||
if (filename != null) {
|
||||
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentColors = { type: WallpaperType? ->
|
||||
// If applying for :
|
||||
// - all themes: no overrides needed
|
||||
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
|
||||
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
|
||||
}
|
||||
val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? ->
|
||||
when {
|
||||
// don't have image in parent or already selected wallpaper with custom image
|
||||
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
|
||||
type is WallpaperType.Image && (wallpaperType is WallpaperType.Image || currentColors(type).wallpaper.type.image == null) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||
type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
|
||||
themeModeOverride.value.type != type || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type)
|
||||
else -> onTypeChange(type)
|
||||
}
|
||||
}
|
||||
|
||||
val editColor = { name: ThemeColor ->
|
||||
editColor(
|
||||
name,
|
||||
wallpaperType,
|
||||
wallpaperImage,
|
||||
onColorChange = { color ->
|
||||
preApplyGlobalIfNeeded(themeModeOverride.value.type)
|
||||
ThemeManager.applyThemeColor(name, color, themeModeOverride)
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
WallpaperPresetSelector(
|
||||
selectedWallpaper = wallpaperType,
|
||||
baseTheme = currentTheme.base,
|
||||
currentColors = { type ->
|
||||
// If applying for :
|
||||
// - all themes: no overrides needed
|
||||
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
|
||||
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
|
||||
},
|
||||
onChooseType = onChooseType
|
||||
)
|
||||
|
||||
WallpaperSetupView(
|
||||
themeModeOverride.value.type,
|
||||
CurrentColors.collectAsState().value.base,
|
||||
currentTheme.wallpaper,
|
||||
currentTheme.appColors.sentMessage,
|
||||
currentTheme.appColors.sentQuote,
|
||||
currentTheme.appColors.receivedMessage,
|
||||
currentTheme.appColors.receivedQuote,
|
||||
editColor = { name -> editColor(name) },
|
||||
onTypeChange = onTypeChange,
|
||||
)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (!globalThemeUsed.value) {
|
||||
ResetToGlobalThemeButton {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
withBGApi { save(applyToMode.value, null) }
|
||||
}
|
||||
}
|
||||
|
||||
SetDefaultThemeButton {
|
||||
globalThemeUsed.value = false
|
||||
val lightBase = DefaultTheme.LIGHT
|
||||
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||
val mode = themeModeOverride.value.mode
|
||||
withBGApi {
|
||||
// Saving for both modes in one place by changing mode once per save
|
||||
if (applyToMode.value == null) {
|
||||
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
|
||||
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
|
||||
}
|
||||
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
|
||||
save(themeModeOverride.value.mode, themeModeOverride.value)
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(theme.mode) {
|
||||
themeModeOverride.value = theme
|
||||
if (applyToMode.value != null) {
|
||||
applyToMode.value = theme.mode
|
||||
}
|
||||
}
|
||||
|
||||
// Applies updated global theme if current one tracks global theme
|
||||
KeyChangeEffect(CurrentColors.collectAsState().value) {
|
||||
if (globalThemeUsed.value) {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (showMore) {
|
||||
val values by remember { mutableStateOf(
|
||||
listOf(
|
||||
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
|
||||
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
|
||||
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
|
||||
)
|
||||
)
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.chat_theme_apply_to_mode),
|
||||
values,
|
||||
applyToMode,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = {
|
||||
applyToMode.value = it
|
||||
if (it != null && it != CurrentColors.value.base.mode) {
|
||||
val lightBase = DefaultTheme.LIGHT
|
||||
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||
} else {
|
||||
AdvancedSettingsButton { showMore = true }
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.ChatWallpaperEditor(
|
||||
theme: ThemeModeOverride,
|
||||
applyToMode: DefaultThemeMode?,
|
||||
globalThemeUsed: MutableState<Boolean>,
|
||||
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
|
||||
mutableStateOf(
|
||||
ThemeManager.currentColors(null, if (themeModeOverride.value == ThemeModeOverride()) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
)
|
||||
}
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.settings_section_title_chat_theme))
|
||||
|
||||
val onTypeCopyFromSameTheme: (WallpaperType?) -> Boolean = { type ->
|
||||
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
|
||||
false
|
||||
} else {
|
||||
val success = ThemeManager.copyFromSameThemeOverrides(type, chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight), themeModeOverride)
|
||||
if (success) {
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
globalThemeUsed.value = false
|
||||
}
|
||||
success
|
||||
}
|
||||
}
|
||||
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
|
||||
if (globalThemeUsed.value) {
|
||||
onTypeCopyFromSameTheme(type)
|
||||
}
|
||||
}
|
||||
val onTypeChange: (WallpaperType?) -> Unit = { type ->
|
||||
if (globalThemeUsed.value) {
|
||||
preApplyGlobalIfNeeded(type)
|
||||
// Saves copied static image instead of original from global theme
|
||||
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
|
||||
} else {
|
||||
ThemeManager.applyWallpaper(type, themeModeOverride)
|
||||
}
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
}
|
||||
|
||||
val editColor: (ThemeColor) -> Unit = { name: ThemeColor ->
|
||||
ModalManager.end.showModal {
|
||||
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
|
||||
mutableStateOf(
|
||||
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
)
|
||||
}
|
||||
val initialColor: Color = when (name) {
|
||||
ThemeColor.WALLPAPER_BACKGROUND -> currentTheme.wallpaper.background ?: Color.Transparent
|
||||
ThemeColor.WALLPAPER_TINT -> currentTheme.wallpaper.tint ?: Color.Transparent
|
||||
ThemeColor.PRIMARY -> currentTheme.colors.primary
|
||||
ThemeColor.PRIMARY_VARIANT -> currentTheme.colors.primaryVariant
|
||||
ThemeColor.SECONDARY -> currentTheme.colors.secondary
|
||||
ThemeColor.SECONDARY_VARIANT -> currentTheme.colors.secondaryVariant
|
||||
ThemeColor.BACKGROUND -> currentTheme.colors.background
|
||||
ThemeColor.SURFACE -> currentTheme.colors.surface
|
||||
ThemeColor.TITLE -> currentTheme.appColors.title
|
||||
ThemeColor.PRIMARY_VARIANT2 -> currentTheme.appColors.primaryVariant2
|
||||
ThemeColor.SENT_MESSAGE -> currentTheme.appColors.sentMessage
|
||||
ThemeColor.SENT_QUOTE -> currentTheme.appColors.sentQuote
|
||||
ThemeColor.RECEIVED_MESSAGE -> currentTheme.appColors.receivedMessage
|
||||
ThemeColor.RECEIVED_QUOTE -> currentTheme.appColors.receivedQuote
|
||||
}
|
||||
AppearanceScope.ColorEditor(
|
||||
name,
|
||||
initialColor,
|
||||
CurrentColors.collectAsState().value.base,
|
||||
themeModeOverride.value.type,
|
||||
themeModeOverride.value.type?.image,
|
||||
currentTheme.wallpaper.background,
|
||||
currentTheme.wallpaper.tint,
|
||||
currentColors = {
|
||||
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
},
|
||||
onColorChange = { color ->
|
||||
preApplyGlobalIfNeeded(themeModeOverride.value.type)
|
||||
ThemeManager.applyThemeColor(name, color, themeModeOverride)
|
||||
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
val filename = saveWallpaperFile(to)
|
||||
if (filename != null) {
|
||||
// Delete only non-user image
|
||||
if (!globalThemeUsed.value) {
|
||||
removeWallpaperFile((themeModeOverride.value.type as? WallpaperType.Image)?.filename)
|
||||
}
|
||||
globalThemeUsed.value = false
|
||||
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentColors = { type: WallpaperType? ->
|
||||
ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
WallpaperPresetSelector(
|
||||
selectedWallpaper = currentTheme.wallpaper.type,
|
||||
activeBackgroundColor = currentTheme.wallpaper.background,
|
||||
activeTintColor = currentTheme.wallpaper.tint,
|
||||
baseTheme = CurrentColors.collectAsState().value.base,
|
||||
currentColors = { type -> currentColors(type) },
|
||||
onChooseType = { type ->
|
||||
when {
|
||||
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
|
||||
type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> {
|
||||
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||
}
|
||||
type is WallpaperType.Image -> {
|
||||
if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) {
|
||||
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||
}
|
||||
}
|
||||
globalThemeUsed.value || themeModeOverride.value.type != type -> {
|
||||
onTypeCopyFromSameTheme(type)
|
||||
}
|
||||
else -> {
|
||||
onTypeChange(type)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
WallpaperSetupView(
|
||||
themeModeOverride.value.type,
|
||||
CurrentColors.collectAsState().value.base,
|
||||
currentTheme.wallpaper,
|
||||
currentTheme.appColors.sentMessage,
|
||||
currentTheme.appColors.sentQuote,
|
||||
currentTheme.appColors.receivedMessage,
|
||||
currentTheme.appColors.receivedQuote,
|
||||
editColor = editColor,
|
||||
onTypeChange = onTypeChange,
|
||||
)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (!globalThemeUsed.value) {
|
||||
ResetToGlobalThemeButton {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
withBGApi { save(applyToMode.value, null) }
|
||||
}
|
||||
}
|
||||
|
||||
SetDefaultThemeButton {
|
||||
globalThemeUsed.value = false
|
||||
val lightBase = DefaultTheme.LIGHT
|
||||
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||
val mode = themeModeOverride.value.mode
|
||||
withBGApi {
|
||||
// Saving for both modes in one place by changing mode once per save
|
||||
if (applyToMode.value == null) {
|
||||
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
|
||||
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
|
||||
}
|
||||
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
|
||||
save(themeModeOverride.value.mode, themeModeOverride.value)
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(theme.mode) {
|
||||
themeModeOverride.value = theme
|
||||
if (applyToMode.value != null) {
|
||||
applyToMode.value = theme.mode
|
||||
}
|
||||
}
|
||||
|
||||
// Applies updated global theme if current one tracks global theme
|
||||
KeyChangeEffect(CurrentColors.collectAsState()) {
|
||||
if (globalThemeUsed.value) {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (showMore) {
|
||||
val values by remember { mutableStateOf(
|
||||
listOf(
|
||||
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
|
||||
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
|
||||
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
|
||||
)
|
||||
)
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.chat_theme_apply_to_mode),
|
||||
values,
|
||||
applyToMode,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = {
|
||||
applyToMode.value = it
|
||||
if (it != null && it != CurrentColors.value.base.mode) {
|
||||
val lightBase = DefaultTheme.LIGHT
|
||||
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||
} else {
|
||||
AdvancedSettingsButton { showMore = true }
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetToGlobalThemeButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(MR.strings.chat_theme_reset_to_global_theme), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetDefaultThemeButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdvancedSettingsButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_arrow_downward),
|
||||
stringResource(MR.strings.wallpaper_advanced_settings),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
+57
-1
@@ -19,6 +19,7 @@ import kotlinx.serialization.encodeToString
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
@@ -280,6 +281,38 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile?
|
||||
}
|
||||
}
|
||||
|
||||
fun saveWallpaperFile(uri: URI): String? {
|
||||
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||
val destFile = File(getWallpaperFilePath(destFileName))
|
||||
try {
|
||||
val inputStream = uri.inputStream()
|
||||
Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error saving wallpaper file: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||
return null
|
||||
}
|
||||
return destFile.name
|
||||
}
|
||||
|
||||
fun saveWallpaperFile(image: ImageBitmap): String {
|
||||
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||
val destFile = File(getWallpaperFilePath(destFileName))
|
||||
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
|
||||
val output = FileOutputStream(destFile)
|
||||
dataResized.use {
|
||||
it.writeTo(output)
|
||||
}
|
||||
return destFile.name
|
||||
}
|
||||
|
||||
fun removeWallpaperFile(fileName: String? = null) {
|
||||
File(getWallpaperFilePath("_")).parentFile.listFiles()?.forEach {
|
||||
if (it.name == fileName) it.delete()
|
||||
}
|
||||
WallpaperType.cachedImages.remove(fileName)
|
||||
}
|
||||
|
||||
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
|
||||
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||
tmpFile.deleteOnExit()
|
||||
@@ -550,10 +583,33 @@ fun KeyChangeEffect(
|
||||
val initialKey = remember { key1 }
|
||||
val initialKey2 = remember { key2 }
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1) {
|
||||
LaunchedEffect(key1, key2) {
|
||||
if (anyChange || key1 != initialKey || key2 != initialKey2) {
|
||||
block()
|
||||
anyChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the [block] only after initial value of the [key1], or [key2], or [key3] changes, not after initial launch
|
||||
* */
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun KeyChangeEffect(
|
||||
key1: Any?,
|
||||
key2: Any?,
|
||||
key3: Any?,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val initialKey = remember { key1 }
|
||||
val initialKey2 = remember { key2 }
|
||||
val initialKey3 = remember { key3 }
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1, key2, key3) {
|
||||
if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) {
|
||||
block()
|
||||
anyChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
||||
} else {
|
||||
val r: LAResult = if (passcode.value == authRequest.password) {
|
||||
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
||||
initChatControllerAndRunMigrations()
|
||||
initChatControllerOnStart()
|
||||
}
|
||||
LAResult.Success
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -669,7 +669,7 @@ private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver:
|
||||
if (state is MigrationToState.ArchiveImportFailed) {
|
||||
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
|
||||
deleteChatDatabaseFilesAndState()
|
||||
initChatControllerAndRunMigrations()
|
||||
initChatControllerOnStart()
|
||||
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
|
||||
stopArchiveDownloading(state.fileId, state.ctrl)
|
||||
}
|
||||
|
||||
+853
-119
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -250,17 +250,17 @@ fun NetworkAndServersView() {
|
||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
}
|
||||
|
||||
// if (currentRemoteHost == null) {
|
||||
// SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
||||
// SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
||||
// SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
||||
// SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
// }
|
||||
// SectionCustomFooter {
|
||||
// Text(stringResource(MR.strings.private_routing_explanation))
|
||||
// }
|
||||
// Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
// }
|
||||
if (currentRemoteHost == null) {
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
||||
SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
||||
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
Text(stringResource(MR.strings.private_routing_explanation))
|
||||
}
|
||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||
}
|
||||
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||
|
||||
+18
-5
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
@@ -63,10 +64,6 @@ fun PrivacySettingsView(
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
|
||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||
})
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_chat_bubble),
|
||||
@@ -91,6 +88,22 @@ fun PrivacySettingsView(
|
||||
chatModel.simplexLinkMode.value = it
|
||||
})
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_files)) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
|
||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||
})
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) {
|
||||
Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers))
|
||||
} else {
|
||||
Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers))
|
||||
}
|
||||
}
|
||||
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (currentUser != null) {
|
||||
@@ -141,7 +154,7 @@ fun PrivacySettingsView(
|
||||
}
|
||||
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionDividerSpaced()
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ fun SettingsLayout(
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }, extraPadding = true)
|
||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
@@ -847,7 +847,6 @@
|
||||
<string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string>
|
||||
<string name="revoke_file__confirm">اسحب الوصول</string>
|
||||
<string name="save_archive">حفظ الأرشيف</string>
|
||||
<string name="save_color">حفظ اللون</string>
|
||||
<string name="reveal_verb">كشف</string>
|
||||
<string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string>
|
||||
<string name="reject_contact_button">رفض</string>
|
||||
|
||||
@@ -119,6 +119,8 @@
|
||||
<string name="error_joining_group">Error joining group</string>
|
||||
<string name="cannot_receive_file">Cannot receive file</string>
|
||||
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
||||
<string name="file_not_approved_title">Unknown servers!</string>
|
||||
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
|
||||
<string name="error_receiving_file">Error receiving file</string>
|
||||
<string name="error_creating_address">Error creating address</string>
|
||||
<string name="contact_already_exists">Contact already exists</string>
|
||||
@@ -744,7 +746,7 @@
|
||||
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="customize_theme_title">Customize theme</string>
|
||||
<string name="theme_colors_section_title">THEME COLORS</string>
|
||||
<string name="theme_colors_section_title">INTERFACE COLORS</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="app_version_name">App version: v%s</string>
|
||||
<string name="app_version_code">App build: %s</string>
|
||||
@@ -992,6 +994,9 @@
|
||||
<string name="protect_app_screen">Protect app screen</string>
|
||||
<string name="encrypt_local_files">Encrypt local files</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="protect_ip_address">Protect IP address</string>
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
<string name="privacy_show_last_messages">Show last messages</string>
|
||||
<string name="privacy_message_draft">Message draft</string>
|
||||
@@ -1056,6 +1061,7 @@
|
||||
<string name="settings_section_title_app">APP</string>
|
||||
<string name="settings_section_title_device">DEVICE</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="settings_section_title_files">FILES</string>
|
||||
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
|
||||
<string name="settings_restart_app">Restart</string>
|
||||
<string name="settings_shutdown">Shutdown</string>
|
||||
@@ -1066,6 +1072,9 @@
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_profile_images">Profile images</string>
|
||||
<string name="settings_section_title_chat_theme">Chat theme</string>
|
||||
<string name="settings_section_title_user_theme">Profile theme</string>
|
||||
<string name="settings_section_title_chat_colors">Chat colors</string>
|
||||
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
|
||||
<string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string>
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
@@ -1195,6 +1204,7 @@
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
<string name="upgrade_and_open_chat">Upgrade and open chat</string>
|
||||
<string name="downgrade_and_open_chat">Downgrade and open chat</string>
|
||||
@@ -1537,23 +1547,30 @@
|
||||
<string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string>
|
||||
|
||||
<!-- Default themes -->
|
||||
<string name="color_mode_system">System</string>
|
||||
<string name="color_mode_light">Light</string>
|
||||
<string name="color_mode_dark">Dark</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
<string name="theme_simplex">SimpleX</string>
|
||||
<string name="theme_black">Black</string>
|
||||
|
||||
<!-- Languages -->
|
||||
<string name="language_system">System</string>
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Theme</string>
|
||||
<string name="color_mode">Color mode</string>
|
||||
<string name="dark_theme">Dark theme</string>
|
||||
<string name="save_color">Save color</string>
|
||||
<string name="dark_mode_colors">Dark mode colors</string>
|
||||
<string name="import_theme">Import theme</string>
|
||||
<string name="import_theme_error">Import theme error</string>
|
||||
<string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string>
|
||||
<string name="export_theme">Export theme</string>
|
||||
<string name="reset_color">Reset colors</string>
|
||||
<string name="reset_single_color">Reset color</string>
|
||||
<string name="theme_destination_all_profiles">All chat profiles</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
<string name="color_primary_variant">Additional accent</string>
|
||||
<string name="color_secondary">Secondary</string>
|
||||
@@ -1561,8 +1578,36 @@
|
||||
<string name="color_background">Background</string>
|
||||
<string name="color_surface">Menus & alerts</string>
|
||||
<string name="color_title">Title</string>
|
||||
<string name="color_primary_variant2">Additional accent 2</string>
|
||||
<string name="color_sent_message">Sent message</string>
|
||||
<string name="color_sent_quote">Sent reply</string>
|
||||
<string name="color_received_message">Received message</string>
|
||||
<string name="color_received_quote">Received reply</string>
|
||||
<string name="color_wallpaper_background">Wallpaper background</string>
|
||||
<string name="color_wallpaper_tint">Wallpaper accent</string>
|
||||
<string name="theme_remove_image">Remove image</string>
|
||||
|
||||
<!-- Backgrounds -->
|
||||
<string name="wallpaper_cats">Cats</string>
|
||||
<string name="wallpaper_flowers">Flowers</string>
|
||||
<string name="wallpaper_hearts">Hearts</string>
|
||||
<string name="wallpaper_kids">Kids</string>
|
||||
<string name="wallpaper_school">School</string>
|
||||
<string name="wallpaper_travel">Travel</string>
|
||||
<string name="wallpaper_preview_hello_alice">Good afternoon!</string>
|
||||
<string name="wallpaper_preview_hello_bob">Good morning!</string>
|
||||
<string name="wallpaper_scale">Scale</string>
|
||||
<string name="wallpaper_scale_repeat">Repeat</string>
|
||||
<string name="wallpaper_scale_fill">Fill</string>
|
||||
<string name="wallpaper_scale_fit">Fit</string>
|
||||
<string name="wallpaper_advanced_settings">Advanced settings</string>
|
||||
<string name="chat_theme_reset_to_global_theme">Reset to global theme</string>
|
||||
<string name="chat_theme_set_default_theme">Set default theme</string>
|
||||
<string name="chat_theme_apply_to_mode">Apply to</string>
|
||||
<string name="chat_theme_apply_to_all_modes">All color modes</string>
|
||||
<string name="chat_theme_apply_to_light_mode">Light mode</string>
|
||||
<string name="chat_theme_apply_to_dark_mode">Dark mode</string>
|
||||
|
||||
|
||||
<!-- Preferences.kt -->
|
||||
<string name="chat_preferences_you_allow">You allow</string>
|
||||
|
||||
@@ -1150,7 +1150,6 @@
|
||||
<string name="network_options_revert">Отмени промените</string>
|
||||
<string name="network_options_save">Запази</string>
|
||||
<string name="reset_color">Нулирай цветовете</string>
|
||||
<string name="save_color">Запази цвета</string>
|
||||
<string name="color_secondary">Вторичен</string>
|
||||
<string name="custom_time_picker_select">Избери</string>
|
||||
<string name="to_start_a_new_chat_help_header">За да започнете нов чат</string>
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
|
||||
<string name="incognito">Inkognito</string>
|
||||
<string name="incognito_random_profile">Váš náhodný profil</string>
|
||||
<string name="save_color">Uložit barvu</string>
|
||||
<string name="reset_color">Obnovit barvu</string>
|
||||
<string name="color_primary">Zbarvení</string>
|
||||
<string name="chat_preferences_you_allow">Povolujete</string>
|
||||
|
||||
@@ -842,7 +842,6 @@
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Design</string>
|
||||
<string name="save_color">Farbe speichern</string>
|
||||
<string name="reset_color">Farben zurücksetzen</string>
|
||||
<string name="color_primary">Akzent</string>
|
||||
<!-- Preferences.kt -->
|
||||
|
||||
@@ -607,7 +607,6 @@
|
||||
<string name="secret_text">secreto</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string>
|
||||
<string name="network_options_reset_to_defaults">Restablecer valores por defecto</string>
|
||||
<string name="save_color">Guardar color</string>
|
||||
<string name="icon_descr_server_status_pending">Pendiente</string>
|
||||
<string name="periodic_notifications">Notificaciones periódicas</string>
|
||||
<string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string>
|
||||
|
||||
@@ -873,7 +873,6 @@
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille – osallistu Weblaten kautta!</string>
|
||||
<string name="profile_password">Profiilin salasana</string>
|
||||
<string name="reset_color">Oletusvärit</string>
|
||||
<string name="save_color">Tallenna väri</string>
|
||||
<string name="prohibit_sending_voice_messages">Estä ääniviestien lähettäminen.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Lähetetyt viestit poistetaan asetetun ajan kuluttua.</string>
|
||||
<string name="add_contact_or_create_group">Aloita uusi keskustelu</string>
|
||||
|
||||
@@ -783,7 +783,6 @@
|
||||
<string name="theme_light">Clair</string>
|
||||
<string name="theme_dark">Sombre</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="save_color">Enregistrer la couleur</string>
|
||||
<string name="reset_color">Réinitialisation des couleurs</string>
|
||||
<string name="color_primary">Principale</string>
|
||||
<string name="chat_preferences_you_allow">Vous autorisez</string>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
<string name="personal_welcome">स्वागत %1$s!</string>
|
||||
<string name="callstate_starting">शुरुआत</string>
|
||||
<string name="send_verb">भेजना</string>
|
||||
<string name="save_color">रंग बचाओ</string>
|
||||
<string name="share_verb">साझा करना</string>
|
||||
<string name="reject_contact_button">अस्वीकार</string>
|
||||
<string name="network_use_onion_hosts_required">आवश्यक</string>
|
||||
|
||||
@@ -1196,7 +1196,6 @@
|
||||
<string name="scan_code">Kód beolvasása</string>
|
||||
<string name="open_port_in_firewall_title">Port megnyitása a tűzfalon</string>
|
||||
<string name="callstate_starting">indítás…</string>
|
||||
<string name="save_color">Szín mentése</string>
|
||||
<string name="settings_shutdown">Leállítás</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">elküldve</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">SOCKS proxy használata</string>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -847,7 +847,6 @@
|
||||
<string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string>
|
||||
<string name="feature_received_prohibited">ricevuto, vietato</string>
|
||||
<string name="reset_color">Ripristina i colori</string>
|
||||
<string name="save_color">Salva colore</string>
|
||||
<string name="accept_feature_set_1_day">Imposta 1 giorno</string>
|
||||
<string name="set_group_preferences">Imposta le preferenze del gruppo</string>
|
||||
<string name="theme">Tema</string>
|
||||
|
||||
@@ -898,7 +898,6 @@
|
||||
<string name="save_passphrase_and_open_chat">שמור סיסמה ופתח את הצ׳אט</string>
|
||||
<string name="save_archive">שמור ארכיון</string>
|
||||
<string name="select_contacts">בחירת אנשי קשר</string>
|
||||
<string name="save_color">שמור צבע</string>
|
||||
<string name="v5_1_self_destruct_passcode">קוד גישה להשמדה עצמית</string>
|
||||
<string name="custom_time_unit_seconds">שניות</string>
|
||||
<string name="custom_time_picker_select">אישור</string>
|
||||
|
||||
@@ -792,7 +792,6 @@
|
||||
<string name="network_options_save">保存</string>
|
||||
<string name="update_network_settings_question">ネットワーク設定を更新しますか?</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string>
|
||||
<string name="save_color">色を保存</string>
|
||||
<string name="chat_preferences_you_allow">あなたが次を許可しています:</string>
|
||||
<string name="chat_preferences_yes">オン</string>
|
||||
<string name="set_group_preferences">グループ設定を行う</string>
|
||||
|
||||
@@ -757,7 +757,6 @@
|
||||
<string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string>
|
||||
<string name="remove_passphrase">지우기</string>
|
||||
<string name="save_archive">아카이브 저장하기</string>
|
||||
<string name="save_color">색상 저장하기</string>
|
||||
<string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string>
|
||||
<string name="restore_database">데이터베이스 백업 복원하기</string>
|
||||
<string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string>
|
||||
|
||||
@@ -189,7 +189,6 @@
|
||||
<string name="dont_show_again">Daugiau neberodyti</string>
|
||||
<string name="theme_dark">Tamsus</string>
|
||||
<string name="reset_color">Atstatyti spalvas</string>
|
||||
<string name="save_color">Įrašyti spalvą</string>
|
||||
<string name="group_preferences">Grupės parinktys</string>
|
||||
<string name="full_deletion">Ištrinti visiems</string>
|
||||
<string name="direct_messages">Tiesioginės žinutės</string>
|
||||
|
||||
@@ -323,7 +323,6 @@
|
||||
<string name="network_options_revert">പഴയപടിയാക്കുക</string>
|
||||
<string name="theme_system">സംവിധാനം</string>
|
||||
<string name="language_system">സംവിധാനം</string>
|
||||
<string name="save_color">നിറം സംരക്ഷിക്കുക</string>
|
||||
<string name="color_title">ശീർഷകം</string>
|
||||
<string name="color_secondary">രണ്ടാംതരമായ</string>
|
||||
<string name="chat_preferences_yes">അതെ</string>
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
<string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string>
|
||||
<string name="theme">Thema</string>
|
||||
<string name="reset_color">Kleuren resetten</string>
|
||||
<string name="save_color">Kleur opslaan</string>
|
||||
<string name="theme_system">Systeem</string>
|
||||
<string name="chat_preferences_yes">ja</string>
|
||||
<string name="feature_received_prohibited">gekregen, verboden</string>
|
||||
|
||||
@@ -799,7 +799,6 @@
|
||||
<string name="prohibit_sending_disappearing_messages">Zabroń wysyłania znikających wiadomości.</string>
|
||||
<string name="feature_received_prohibited">otrzymane, zabronione</string>
|
||||
<string name="reset_color">Resetuj kolory</string>
|
||||
<string name="save_color">Zapisz kolor</string>
|
||||
<string name="accept_feature_set_1_day">Ustaw 1 dzień</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="language_system">System</string>
|
||||
|
||||
@@ -485,7 +485,6 @@
|
||||
<string name="network_options_revert">Reverter</string>
|
||||
<string name="network_options_save">Salvar</string>
|
||||
<string name="reset_color">Redefinir cores</string>
|
||||
<string name="save_color">Salvar cor</string>
|
||||
<string name="v4_5_italian_interface">interface italiana</string>
|
||||
<string name="periodic_notifications">Notificações periódicas</string>
|
||||
<string name="use_camera_button">Câmera</string>
|
||||
|
||||
@@ -370,7 +370,6 @@
|
||||
<string name="invite_prohibited_description">Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal</string>
|
||||
<string name="info_row_connection">Conexão</string>
|
||||
<string name="incognito_info_protects">O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado.</string>
|
||||
<string name="save_color">Salvar cor</string>
|
||||
<string name="description_you_shared_one_time_link">você partilhou ligação de utilização única</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">você partilhou ligação anónima de utilização única</string>
|
||||
<string name="invalid_connection_link">Ligação de conexão inválida</string>
|
||||
|
||||
@@ -848,7 +848,6 @@
|
||||
<string name="theme_dark">Темная</string>
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Тема</string>
|
||||
<string name="save_color">Сохранить цвет</string>
|
||||
<string name="reset_color">Сбросить цвета</string>
|
||||
<string name="color_primary">Акцент</string>
|
||||
<!-- Preferences.kt -->
|
||||
|
||||
@@ -855,7 +855,6 @@
|
||||
<string name="network_options_revert">เปลี่ยนกลับ</string>
|
||||
<string name="network_options_save">บันทึก</string>
|
||||
<string name="reset_color">รีเซ็ตสี</string>
|
||||
<string name="save_color">บันทึกสี</string>
|
||||
<string name="feature_received_prohibited">ได้รับ, ห้าม</string>
|
||||
<string name="v4_4_live_messages_desc">ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</string>
|
||||
<string name="v4_5_reduced_battery_usage">ลดการใช้แบตเตอรี่</string>
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
<string name="theme_simplex">SimpleX</string>
|
||||
<string name="dark_theme">Koyu tema</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="save_color">Rengi kaydet</string>
|
||||
<string name="import_theme">Temayı içe aktar</string>
|
||||
<string name="import_theme_error">Temayı içe aktarırken hata oluştu</string>
|
||||
<string name="import_theme_error_desc">Dosyanın doğru YAML sözdizimine sahip olduğundan emin olun. Tema dosyası yapısının bir örneğine sahip olmak için temayı dışa aktarın.</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user