mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed16f71ee4 | |||
| 5bc70bdfed | |||
| fb4927ca87 | |||
| 2023464a13 | |||
| b8bf5871fb | |||
| 1573b7af13 | |||
| a5bf3cbca8 | |||
| d5529031ee | |||
| d244338b13 | |||
| ce55c84b96 | |||
| c52f9baefa | |||
| ea1c5da65d | |||
| 5695bb9d46 | |||
| 13751d5705 | |||
| 02803b1491 | |||
| 222af69a71 | |||
| 5e96e1ea2b | |||
| 490e8cead8 | |||
| f578ee843b | |||
| cf2eb76cb7 | |||
| 48f9dcab9a | |||
| ac2ef54838 | |||
| 7e269c6b8c | |||
| 3e739924bb | |||
| 83706b19da | |||
| 6f19a7a20f | |||
| a8740cee4b | |||
| 4000fe383d | |||
| 75445e604e | |||
| 0596278e50 | |||
| 7a77775a42 | |||
| dc8d08ec04 | |||
| 4ad0e0f65e | |||
| d01515e755 | |||
| 3fa70ba217 | |||
| 6ae4cddc2e | |||
| ac8ee971f5 | |||
| 11d4872186 | |||
| 370e2e87ad | |||
| ef30fcf5e2 | |||
| 6ad1423f29 | |||
| c52e5349db | |||
| 3c98c6acc0 | |||
| c251f5e2bd | |||
| b165603136 | |||
| 184a24b70e | |||
| d46bae6498 | |||
| 5ac521f6d8 | |||
| 7b5b747b19 | |||
| 037655e30f | |||
| 54088c6d5a | |||
| 78f7fcc89f | |||
| b402caeb89 | |||
| ff8a8de3c8 | |||
| d22c0c2614 | |||
| 73c8d3c2e3 | |||
| 0110385e65 | |||
| ad0a678c5f | |||
| 0495a4cd78 | |||
| 02267c5263 | |||
| cad3bc048f | |||
| 1b04423745 | |||
| 2143eb2d7a | |||
| 3d395b0e45 | |||
| d79b4dfb5a | |||
| 0ae9f5ebba | |||
| 57c80786dc | |||
| da1cad96fc | |||
| b58ccedf23 | |||
| 01cadefde7 | |||
| c70e7223d9 | |||
| a1d4f52185 | |||
| 373070f388 | |||
| a03ccd0ba6 | |||
| a7982cccf9 | |||
| dccd6631af | |||
| b0516b6440 | |||
| bf91666f07 | |||
| ab47a5a27e | |||
| 23581f59ae | |||
| 291df6e9d0 | |||
| dae0b63c22 | |||
| 0964982782 | |||
| 9eb6034e1a |
@@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent and important updates:
|
||||
|
||||
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
||||
|
||||
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
||||
|
||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||
@@ -382,10 +384,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
||||
- ✅ Private notes.
|
||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
||||
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- 🏗 Improve stability and reduce battery usage.
|
||||
- 🏗 Improve experience for the new users.
|
||||
- 🏗 Large groups, communities and public channels.
|
||||
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- Privacy & security slider - a simple way to set all settings at once.
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- Include optional message into connection request sent via contact address.
|
||||
|
||||
@@ -17,6 +17,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
||||
removePasscodesIfReinstalled()
|
||||
prepareForLaunch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -141,6 +142,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareForLaunch() {
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
static func keepScreenOn(_ on: Bool) {
|
||||
UIApplication.shared.isIdleTimerDisabled = on
|
||||
}
|
||||
@@ -148,13 +153,15 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
static var windowStatic: UIWindow?
|
||||
var windowScene: UIWindowScene?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
UITableView.appearance().backgroundColor = .clear
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
self.windowScene = windowScene
|
||||
window = windowScene.keyWindow
|
||||
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
|
||||
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
SceneDelegate.windowStatic = windowScene.keyWindow
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_cats@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_flowers@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_hearts@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_kids@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_school@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_travel@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -14,6 +14,8 @@ struct ContentView: View {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
|
||||
var contentAccessAuthenticationExtended: Bool
|
||||
|
||||
@@ -51,6 +53,16 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
allViews()
|
||||
.scrollContentBackground(.hidden)
|
||||
} else {
|
||||
// on iOS 15 scroll view background disabled in SceneDelegate
|
||||
allViews()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func allViews() -> some View {
|
||||
ZStack {
|
||||
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
|
||||
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
|
||||
@@ -96,6 +108,8 @@ struct ContentView: View {
|
||||
initializationView()
|
||||
}
|
||||
}
|
||||
//.tint(theme.colors.primary)
|
||||
.background(theme.colors.background)
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
@@ -138,6 +152,11 @@ struct ContentView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
.onChange(of: colorScheme) { scheme in
|
||||
if sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified {
|
||||
reactOnDarkThemeChanges(scheme == .dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contentView() -> some View {
|
||||
@@ -224,8 +243,8 @@ struct ContentView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.background)
|
||||
)
|
||||
.fill(theme.colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
private func mainView() -> some View {
|
||||
|
||||
@@ -422,6 +422,16 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
|
||||
guard var current = currentUser else { return }
|
||||
current.uiThemes = uiThemes
|
||||
let i = users.firstIndex(where: { $0.user.userId == current.userId })
|
||||
if let i {
|
||||
users[i].user = current
|
||||
}
|
||||
currentUser = current
|
||||
}
|
||||
|
||||
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
|
||||
@@ -205,6 +205,43 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
|
||||
}
|
||||
}
|
||||
|
||||
func saveWallpaperFile(url: URL) -> String? {
|
||||
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true))
|
||||
do {
|
||||
try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path)
|
||||
return destFile.lastPathComponent
|
||||
} catch {
|
||||
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func saveWallpaperFile(image: UIImage) -> String? {
|
||||
let hasAlpha = imageHasAlpha(image)
|
||||
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true))
|
||||
let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha)
|
||||
do {
|
||||
try dataResized!.write(to: destFile)
|
||||
return destFile.lastPathComponent
|
||||
} catch {
|
||||
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeWallpaperFile(fileName: String? = nil) {
|
||||
do {
|
||||
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach {
|
||||
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) }
|
||||
}
|
||||
} catch {
|
||||
logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)")
|
||||
}
|
||||
if let fileName {
|
||||
WallpaperType.cachedImages.removeValue(forKey: fileName)
|
||||
}
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
|
||||
}
|
||||
|
||||
@@ -242,14 +242,8 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
|
||||
logger.error("apiSuspendChat error: \(String(describing: r))")
|
||||
}
|
||||
|
||||
func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl)
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -561,6 +555,18 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSwitchContact(contactId: Int64) throws -> ConnectionStats {
|
||||
let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId))
|
||||
if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats }
|
||||
@@ -819,6 +825,21 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool {
|
||||
let r = await chatSendCmd(.apiSetUserUIThemes(userId: userId, themes: themes))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("apiSetUserUIThemes bad response: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bool {
|
||||
let r = await chatSendCmd(.apiSetChatUIThemes(chatId: chatId, themes: themes))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("apiSetChatUIThemes bad response: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let userId = try currentUserId("apiCreateUserAddress")
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
@@ -1265,7 +1286,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
|
||||
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
||||
return ChatModel.shared.chats
|
||||
.compactMap{ $0.chatInfo.contact }
|
||||
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
|
||||
.filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) }
|
||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
}
|
||||
|
||||
@@ -1341,8 +1362,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
if encryptionStartedDefault.get() {
|
||||
encryptionStartedDefault.set(false)
|
||||
}
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
@@ -1427,8 +1447,7 @@ func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
|
||||
logger.debug("startChatWithTemporaryDatabase")
|
||||
let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl)
|
||||
try setNetworkConfig(getNetCfg(), ctrl: ctrl)
|
||||
try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl)
|
||||
_ = try apiStartChat(ctrl: ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
@@ -1814,11 +1833,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .rcvFileError(user, aChatItem, _):
|
||||
case let .rcvFileError(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .rcvFileWarning(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
@@ -1835,11 +1858,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileError(user, aChatItem, _):
|
||||
case let .sndFileError(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .sndFileWarning(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .callInvitation(invitation):
|
||||
await MainActor.run {
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
@@ -1937,12 +1964,28 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
|
||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
||||
}
|
||||
case .remoteCtrlStopped:
|
||||
case let .remoteCtrlStopped(_, rcStopReason):
|
||||
// This delay is needed to cancel the session that fails on network failure,
|
||||
// e.g. when user did not grant permission to access local network yet.
|
||||
if let sess = m.remoteCtrlSession {
|
||||
await MainActor.run {
|
||||
m.remoteCtrlSession = nil
|
||||
dismissAllSheets() {
|
||||
switch rcStopReason {
|
||||
case .connectionFailed(.errorAgent(.RCP(.identity))):
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Connection with desktop stopped",
|
||||
message: "This link was used with another mobile device, please create a new link on the desktop."
|
||||
)
|
||||
default:
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Connection with desktop stopped"),
|
||||
message: Text("Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers."),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) }
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
if case .connected = sess.sessionState {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
|
||||
@@ -39,6 +39,7 @@ struct SimpleXApp: App {
|
||||
// so that it's computed by the time view renders, and not on event after rendering
|
||||
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
|
||||
.environmentObject(chatModel)
|
||||
.environmentObject(AppTheme.shared)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// ThemeShared.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) {
|
||||
didSet {
|
||||
AppTheme.shared.name = CurrentColors.name
|
||||
AppTheme.shared.base = CurrentColors.base
|
||||
AppTheme.shared.colors.updateColorsFrom(CurrentColors.colors)
|
||||
AppTheme.shared.appColors.updateColorsFrom(CurrentColors.appColors)
|
||||
AppTheme.shared.wallpaper.updateWallpaperFrom(CurrentColors.wallpaper)
|
||||
AppTheme.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } }
|
||||
var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 }
|
||||
|
||||
func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight }
|
||||
|
||||
class AppTheme: ObservableObject {
|
||||
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
|
||||
|
||||
var name: String
|
||||
var base: DefaultTheme
|
||||
@ObservedObject var colors: Colors
|
||||
@ObservedObject var appColors: AppColors
|
||||
@ObservedObject var wallpaper: AppWallpaper
|
||||
|
||||
init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) {
|
||||
self.name = name
|
||||
self.base = base
|
||||
self.colors = colors
|
||||
self.appColors = appColors
|
||||
self.wallpaper = wallpaper
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemedBackground: ViewModifier {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
theme.base == DefaultTheme.SIMPLEX
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
theme.colors.background.lighter(0.4),
|
||||
theme.colors.background.darker(0.4)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
: LinearGradient(
|
||||
colors: [],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.background(
|
||||
theme.base == DefaultTheme.SIMPLEX
|
||||
? Color.clear
|
||||
: theme.colors.background
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func reactOnDarkThemeChanges(_ isDark: Bool) {
|
||||
systemInDarkThemeCurrently = isDark
|
||||
//sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == isDark {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
extension ThemeWallpaper {
|
||||
public func importFromString() -> ThemeWallpaper {
|
||||
if preset == nil, let image {
|
||||
// Need to save image from string and to save its path
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let parsed = UIImage(data: data),
|
||||
let filename = saveWallpaperFile(image: parsed) {
|
||||
var copy = self
|
||||
copy.image = nil
|
||||
copy.imageFile = filename
|
||||
return copy
|
||||
} else {
|
||||
return ThemeWallpaper()
|
||||
}
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
func withFilledWallpaperBase64() -> ThemeWallpaper {
|
||||
let aw = toAppWallpaper()
|
||||
let type = aw.type
|
||||
let preset: String? = if case let WallpaperType.Preset(filename, _) = type { filename } else { nil }
|
||||
let scale: Float? = if case let WallpaperType.Preset(_, scale) = type { scale } else { if case let WallpaperType.Image(_, scale, _) = type { scale } else { 1.0 } }
|
||||
let scaleType: WallpaperScaleType? = if case let WallpaperType.Image(_, _, scaleType) = type { scaleType } else { nil }
|
||||
let image: String? = if case WallpaperType.Image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
|
||||
return ThemeWallpaper (
|
||||
preset: preset,
|
||||
scale: scale,
|
||||
scaleType: scaleType,
|
||||
background: aw.background?.toReadableHex(),
|
||||
tint: aw.tint?.toReadableHex(),
|
||||
image: image,
|
||||
imageFile: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// ThemeManager.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 03.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
class ThemeManager {
|
||||
struct ActiveTheme {
|
||||
let name: String
|
||||
let base: DefaultTheme
|
||||
let colors: Colors
|
||||
let appColors: AppColors
|
||||
var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .Empty)
|
||||
}
|
||||
|
||||
private static func systemDarkThemeColors() -> (Colors, DefaultTheme) {
|
||||
switch systemDarkThemeDefault.get() {
|
||||
case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK)
|
||||
case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX)
|
||||
case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK)
|
||||
default: (SimplexColorPalette, DefaultTheme.SIMPLEX)
|
||||
}
|
||||
}
|
||||
|
||||
private static func nonSystemThemeName() -> String {
|
||||
let themeName = currentThemeDefault.get()
|
||||
return if themeName != DefaultTheme.SYSTEM_THEME_NAME {
|
||||
themeName
|
||||
} else {
|
||||
systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? {
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName]
|
||||
return appSettingsTheme.getTheme(defaultThemeId)
|
||||
}
|
||||
|
||||
static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride {
|
||||
let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light
|
||||
if let perUserTheme {
|
||||
return perUserTheme
|
||||
}
|
||||
let defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
|
||||
}
|
||||
|
||||
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
|
||||
let themeName = currentThemeDefault.get()
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
|
||||
let baseTheme = switch nonSystemThemeName {
|
||||
case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
|
||||
case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK)))
|
||||
case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX)))
|
||||
case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK)))
|
||||
default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
|
||||
}
|
||||
|
||||
let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark
|
||||
let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme
|
||||
|
||||
if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil {
|
||||
return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper)
|
||||
}
|
||||
let presetWallpaperTheme: ThemeColors? = if let wallpaper = perChatTheme?.wallpaper {
|
||||
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
} else if let wallpaper = perUserTheme?.wallpaper {
|
||||
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
} else {
|
||||
if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
}
|
||||
|
||||
let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base)
|
||||
let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
|
||||
return ActiveTheme(
|
||||
name: themeName,
|
||||
base: baseTheme.base,
|
||||
colors: colors,
|
||||
appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
|
||||
wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
static func currentThemeOverridesForExport(_ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
|
||||
let current = currentColors(nil, perChatTheme, perUserTheme, themeOverridesDefault.get())
|
||||
let wType = current.wallpaper.type
|
||||
let wBackground = current.wallpaper.background
|
||||
let wTint = current.wallpaper.tint
|
||||
let w: ThemeWallpaper? = if case WallpaperType.Empty = wType {
|
||||
nil
|
||||
} else {
|
||||
ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64()
|
||||
}
|
||||
return ThemeOverrides(
|
||||
themeId: "",
|
||||
base: current.base,
|
||||
colors: ThemeColors.from(current.colors, current.appColors),
|
||||
wallpaper: w
|
||||
)
|
||||
}
|
||||
|
||||
static func applyTheme(_ theme: String) {
|
||||
currentThemeDefault.set(theme)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
SceneDelegate.windowStatic?.tintColor = UIColor(CurrentColors.colors.primary)
|
||||
SceneDelegate.windowStatic?.backgroundColor = UIColor(CurrentColors.colors.background)
|
||||
SceneDelegate.windowStatic?.overrideUserInterfaceStyle = switch currentThemeDefault.get() {
|
||||
case DefaultTheme.LIGHT.themeName: .light
|
||||
case DefaultTheme.SYSTEM_THEME_NAME: .unspecified
|
||||
default: .dark
|
||||
}
|
||||
}
|
||||
|
||||
static func changeDarkTheme(_ theme: String) {
|
||||
systemDarkThemeDefault.set(theme)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let nonSystemThemeName = baseTheme.themeName
|
||||
let pref = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
let themeId = currentThemeIdsDefault.get()[nonSystemThemeName]
|
||||
let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme)
|
||||
pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding<ThemeModeOverride>) {
|
||||
pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex())
|
||||
}
|
||||
|
||||
static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) {
|
||||
let nonSystemThemeName = baseTheme.themeName
|
||||
let pref = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
let theme = overrides.sameTheme(type, baseTheme.themeName)
|
||||
var prevValue = theme ?? ThemeOverrides(base: baseTheme)
|
||||
prevValue.wallpaper = if let type {
|
||||
if case WallpaperType.Empty = type {
|
||||
nil as ThemeWallpaper?
|
||||
} else {
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
pref.set(overrides.replace(prevValue))
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
CurrentColors = currentColors( nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
|
||||
let overrides = themeOverridesDefault.get()
|
||||
let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true {
|
||||
wallpaper
|
||||
} else {
|
||||
overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper
|
||||
}
|
||||
guard let sameWallpaper else {
|
||||
if let type {
|
||||
var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil)
|
||||
w.scale = nil
|
||||
w.scaleType = nil
|
||||
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w)
|
||||
} else {
|
||||
// Make an empty wallpaper to override any top level ones
|
||||
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper())
|
||||
}
|
||||
return true
|
||||
}
|
||||
var type = sameWallpaper.toAppWallpaper().type
|
||||
if case let WallpaperType.Image(filename, scale, scaleType) = type, sameWallpaper.imageFile == 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
|
||||
if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) {
|
||||
type = WallpaperType.Image(filename, scale, scaleType)
|
||||
} else {
|
||||
logger.error("Error while copying wallpaper from global overrides to chat overrides")
|
||||
return false
|
||||
}
|
||||
}
|
||||
var prevValue = pref.wrappedValue
|
||||
var w = ThemeWallpaper.from(type, nil, nil)
|
||||
w.scale = nil
|
||||
w.scaleType = nil
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper = w
|
||||
pref.wrappedValue = prevValue
|
||||
return true
|
||||
}
|
||||
|
||||
static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding<ThemeModeOverride>) {
|
||||
var prevValue = pref.wrappedValue
|
||||
prevValue.wallpaper = if let type {
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
pref.wrappedValue = prevValue
|
||||
}
|
||||
|
||||
static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let wallpaper = theme.wallpaper?.importFromString()
|
||||
let nonSystemThemeName = theme.base.themeName
|
||||
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base)
|
||||
if let imageFile = prevValue.wallpaper?.imageFile {
|
||||
try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile))
|
||||
}
|
||||
prevValue.base = theme.base
|
||||
prevValue.colors = theme.colors
|
||||
prevValue.wallpaper = wallpaper
|
||||
pref.set(overrides.replace(prevValue))
|
||||
currentThemeDefault.set(nonSystemThemeName)
|
||||
var currentThemeIds = currentThemeIdsDefault.get()
|
||||
currentThemeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(currentThemeIds)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName],
|
||||
var prevValue = overrides.getTheme(themeId)
|
||||
else { return }
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper?.background = nil
|
||||
prevValue.wallpaper?.tint = nil
|
||||
pref.set(overrides.replace(prevValue))
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: Binding<ThemeModeOverride>) {
|
||||
var prevValue = pref.wrappedValue
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper?.background = nil
|
||||
prevValue.wallpaper?.tint = nil
|
||||
pref.wrappedValue = prevValue
|
||||
}
|
||||
|
||||
static func removeTheme(_ themeId: String?) {
|
||||
var themes = themeOverridesDefault.get().map { $0 }
|
||||
themes.removeAll(where: { $0.themeId == themeId })
|
||||
themeOverridesDefault.set(themes)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// MPVolumeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Stanislav on 24.04.2024.
|
||||
// Created by Avently on 24.04.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct IncomingCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var cc = CallController.shared
|
||||
|
||||
var body: some View {
|
||||
@@ -43,7 +44,7 @@ struct IncomingCallView: View {
|
||||
cc.endCall(invitation: invitation)
|
||||
}
|
||||
|
||||
callButton("Ignore", "multiply", .accentColor) {
|
||||
callButton("Ignore", "multiply", theme.colors.primary) {
|
||||
cc.activeCallInvitation = nil
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ struct IncomingCallView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
.onAppear { dismissAllSheets() }
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ enum SendReceipts: Identifiable, Hashable {
|
||||
|
||||
struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@State var contact: Contact
|
||||
@@ -110,6 +111,7 @@ struct ChatInfoView: View {
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
@@ -119,6 +121,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .queueInfo(info): return "queueInfo \(info)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -224,9 +227,22 @@ struct ChatInfoView: View {
|
||||
Section(header: Text("For console")) {
|
||||
infoRow("Local name", chat.chatInfo.localDisplayName)
|
||||
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
||||
Button ("Debug delivery") {
|
||||
Task {
|
||||
do {
|
||||
let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId))
|
||||
await MainActor.run { alert = .queueInfo(info: info) }
|
||||
} catch let e {
|
||||
logger.error("apiContactQueueInfo error: \(responseError(e))")
|
||||
let a = getErrorAlert(e, "Error")
|
||||
await MainActor.run { alert = .error(title: a.title, error: a.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -243,6 +259,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
@@ -355,6 +372,7 @@ struct ChatInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
contact.verified ? "View security code" : "Verify security code",
|
||||
@@ -371,6 +389,7 @@ struct ChatInfoView: View {
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
|
||||
)
|
||||
.navigationBarTitle("Contact preferences")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Contact preferences", systemImage: "switch.2")
|
||||
@@ -419,7 +438,7 @@ struct ChatInfoView: View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
@@ -577,6 +596,22 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
|
||||
)
|
||||
}
|
||||
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
|
||||
let (rcvMsgInfo, qInfo) = info
|
||||
var msgInfo: String
|
||||
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
|
||||
return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo)
|
||||
}
|
||||
|
||||
func queueInfoAlert(_ info: String) -> Alert {
|
||||
Alert(
|
||||
title: Text("Message queue info"),
|
||||
message: Text(info),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info }
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var status: CICallStatus
|
||||
@@ -35,7 +36,7 @@ struct CICallItemView: View {
|
||||
case .error: missedCallIcon(sent).foregroundColor(.orange)
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIFeaturePreferenceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var feature: ChatFeature
|
||||
var allowed: FeatureAllowed
|
||||
@@ -45,7 +46,7 @@ struct CIFeaturePreferenceView: View {
|
||||
r = r
|
||||
+ Text(acceptText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct CIFileView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
|
||||
@@ -54,16 +55,18 @@ struct CIFileView: View {
|
||||
switch (file.fileStatus) {
|
||||
case .sndStored: return file.fileProtocol == .local
|
||||
case .sndTransfer: return false
|
||||
case .sndComplete: return false
|
||||
case .sndComplete: return true
|
||||
case .sndCancelled: return false
|
||||
case .sndError: return false
|
||||
case .sndError: return true
|
||||
case .sndWarning: return true
|
||||
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
|
||||
case .rcvError: return true
|
||||
case .rcvWarning: return true
|
||||
case .invalid: return false
|
||||
}
|
||||
}
|
||||
@@ -108,11 +111,40 @@ struct CIFileView: View {
|
||||
if let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case let .rcvError(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .rcvWarning(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case .sndStored:
|
||||
logger.debug("CIFileView fileAction - in .sndStored")
|
||||
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case .sndComplete:
|
||||
logger.debug("CIFileView fileAction - in .sndComplete")
|
||||
if let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case let .sndError(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
case let .sndWarning(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -136,9 +168,10 @@ struct CIFileView: View {
|
||||
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid(file) {
|
||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
|
||||
} else {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
}
|
||||
@@ -150,10 +183,11 @@ struct CIFileView: View {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted:
|
||||
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
|
||||
fileIcon("doc.fill", color: theme.colors.primary, 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)
|
||||
case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIGroupInvitationView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var groupInvitation: CIGroupInvitation
|
||||
@@ -42,7 +42,7 @@ struct CIGroupInvitationView: View {
|
||||
.overlay(DetermineWidth())
|
||||
(
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
@@ -65,11 +65,11 @@ struct CIGroupInvitationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
@@ -99,7 +99,7 @@ struct CIGroupInvitationView: View {
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
var color: Color
|
||||
if action && !inProgress {
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
color = chatIncognito ? .indigo : theme.colors.primary
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,26 @@ struct CIImageView: View {
|
||||
case .rcvTransfer: () // ?
|
||||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
case let .rcvError(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .rcvWarning(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .sndError(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
case let .sndWarning(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
@@ -99,14 +119,16 @@ struct CIImageView: View {
|
||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
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 .rcvComplete: EmptyView()
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ struct CIInvalidJSONView: View {
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.sheet(isPresented: $showJSON) {
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func invalidJSONView(_ json: String) -> some View {
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIMemberCreatedContactView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -43,7 +44,7 @@ struct CIMemberCreatedContactView: View {
|
||||
r = r
|
||||
+ Text(openText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
|
||||
@@ -11,8 +11,9 @@ import SimpleXChat
|
||||
|
||||
struct CIMetaView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var metaColor = Color.secondary
|
||||
var metaColor: Color
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
@@ -63,6 +64,7 @@ func ciMetaText(
|
||||
chatTTL: Int?,
|
||||
encrypted: Bool?,
|
||||
color: Color = .clear,
|
||||
primaryColor: Color = .accentColor,
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
@@ -85,7 +87,7 @@ func ciMetaText(
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (icon, statusColor) = meta.statusIcon(color) {
|
||||
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
let gap = Text(" ").kerning(-1.25)
|
||||
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
|
||||
@@ -112,15 +114,16 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
}
|
||||
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
static let metaColor = Color.secondary
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 100))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con
|
||||
|
||||
struct CIRcvDecryptionError: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
@@ -114,18 +115,18 @@ struct CIRcvDecryptionError: View {
|
||||
}
|
||||
(
|
||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text("Fix connection")
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
@@ -145,7 +146,7 @@ struct CIRcvDecryptionError: View {
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
|
||||
@@ -192,7 +192,7 @@ struct CIVideoView: View {
|
||||
.disabled(!canBePlayed)
|
||||
}
|
||||
}
|
||||
loadingIndicator()
|
||||
fileStatusIcon()
|
||||
}
|
||||
.onAppear {
|
||||
addObserver(player, url)
|
||||
@@ -258,11 +258,11 @@ struct CIVideoView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: w)
|
||||
loadingIndicator()
|
||||
fileStatusIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func loadingIndicator() -> some View {
|
||||
@ViewBuilder private func fileStatusIcon() -> some View {
|
||||
if let file = chatItem.file {
|
||||
switch file.fileStatus {
|
||||
case .sndStored:
|
||||
@@ -279,7 +279,22 @@ struct CIVideoView: View {
|
||||
}
|
||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case let .sndError(sndFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
@@ -289,10 +304,25 @@ struct CIVideoView: View {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
case .rcvComplete: EmptyView()
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case let .rcvError(rcvFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIVoiceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
let recordingFile: CIFile?
|
||||
let duration: Int
|
||||
@@ -92,7 +93,7 @@ struct CIVoiceView: View {
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ struct VoiceMessagePlayerTime: View {
|
||||
|
||||
struct VoiceMessagePlayer: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var recordingFile: CIFile?
|
||||
var recordingTime: TimeInterval
|
||||
@@ -134,18 +135,53 @@ struct VoiceMessagePlayer: View {
|
||||
ZStack {
|
||||
if let recordingFile = recordingFile {
|
||||
switch recordingFile.fileStatus {
|
||||
case .sndStored: playbackButton()
|
||||
case .sndTransfer: playbackButton()
|
||||
case .sndStored:
|
||||
if recordingFile.fileProtocol == .local {
|
||||
playbackButton()
|
||||
} else {
|
||||
loadingIcon()
|
||||
}
|
||||
case .sndTransfer: loadingIcon()
|
||||
case .sndComplete: playbackButton()
|
||||
case .sndCancelled: playbackButton()
|
||||
case .sndError: playbackButton()
|
||||
case let .sndError(sndFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
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))
|
||||
case let .rcvError(rcvFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
} else {
|
||||
@@ -209,7 +245,7 @@ struct VoiceMessagePlayer: View {
|
||||
.foregroundColor(color)
|
||||
.padding(.leading, image == "play.fill" ? 4 : 0)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
if recordingTime > 0 {
|
||||
ProgressCircle(length: recordingTime, progress: $playbackTime)
|
||||
@@ -231,6 +267,7 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
|
||||
private struct ProgressCircle: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -238,7 +275,7 @@ struct VoiceMessagePlayer: View {
|
||||
Circle()
|
||||
.trim(from: 0, to: ((progress ?? TimeInterval(0)) / length))
|
||||
.stroke(
|
||||
Color.accentColor,
|
||||
theme.colors.primary,
|
||||
style: StrokeStyle(lineWidth: 3)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
@@ -246,11 +283,22 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private func loadingIcon() -> some View {
|
||||
ProgressView()
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct DeletedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -19,12 +19,12 @@ struct DeletedItemView: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct EmojiItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -18,7 +19,7 @@ struct EmojiItemView: View {
|
||||
emojiText(chatItem.content.text)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 6)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@@ -9,16 +9,9 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
|
||||
let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
|
||||
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
|
||||
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
|
||||
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
|
||||
private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09)
|
||||
|
||||
struct FramedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
@@ -83,7 +76,10 @@ struct FramedItemView: View {
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
.onAppear {
|
||||
metaColor = metaColor == .secondary ? theme.colors.secondary : metaColor
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
@@ -175,13 +171,13 @@ struct FramedItemView: View {
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 6)
|
||||
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
@@ -233,7 +229,7 @@ struct FramedItemView: View {
|
||||
// if enable this always, size of the framed voice message item will be incorrect after end of playback
|
||||
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
@@ -248,7 +244,7 @@ struct FramedItemView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(sender)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.lineLimit(1)
|
||||
ciQuotedMsgTextView(qi, lines: 2)
|
||||
}
|
||||
@@ -363,22 +359,22 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
onlyImageOrVideo(ci)
|
||||
? Color.clear
|
||||
: chatItemFrameColor(ci, colorScheme)
|
||||
: chatItemFrameColor(ci, theme)
|
||||
}
|
||||
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
}
|
||||
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark)
|
||||
: Color(uiColor: .quaternarySystemFill)
|
||||
? theme.appColors.sentQuote
|
||||
: theme.appColors.receivedQuote
|
||||
}
|
||||
|
||||
struct FramedItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var msgError: MsgErrorType
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -54,6 +55,7 @@ struct IntegrityErrorItemView: View {
|
||||
|
||||
struct CIMsgError: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -62,7 +64,7 @@ struct CIMsgError: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct MarkedDeletedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
@@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListView() -> some View {
|
||||
@@ -59,9 +60,13 @@ struct ChatItemForwardingView: View {
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
} else {
|
||||
emptyList()
|
||||
ZStack {
|
||||
emptyList()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import SimpleXChat
|
||||
struct ChatItemInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@State private var selection: CIInfoTab = .history
|
||||
@State private var alert: CIInfoViewAlert? = nil
|
||||
@State private var messageStatusLimited: Bool = true
|
||||
@State private var fileStatusLimited: Bool = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum CIInfoTab {
|
||||
@@ -99,12 +101,14 @@ struct ChatItemInfoView: View {
|
||||
Label("History", systemImage: "clock")
|
||||
}
|
||||
.tag(CIInfoTab.history)
|
||||
.modifier(ThemedBackground())
|
||||
if let qi = ci.quotedItem {
|
||||
quoteTab(qi)
|
||||
.tabItem {
|
||||
Label("In reply to", systemImage: "arrowshape.turn.up.left")
|
||||
}
|
||||
.tag(CIInfoTab.quote)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem {
|
||||
forwardedFromTab(forwardedFromItem)
|
||||
@@ -112,6 +116,7 @@ struct ChatItemInfoView: View {
|
||||
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
|
||||
}
|
||||
.tag(CIInfoTab.forwarded)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -121,6 +126,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
} else {
|
||||
historyTab()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +163,35 @@ struct ChatItemInfoView: View {
|
||||
if developerTools {
|
||||
infoRow("Database ID", "\(meta.itemId)")
|
||||
infoRow("Record updated at", localTimestamp(meta.updatedAt))
|
||||
let msv = infoRow("Message status", ci.meta.itemStatus.id)
|
||||
Group {
|
||||
if messageStatusLimited {
|
||||
msv.lineLimit(1)
|
||||
} else {
|
||||
msv
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
messageStatusLimited.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
if let file = ci.file {
|
||||
let fsv = infoRow("File status", file.fileStatus.id)
|
||||
Group {
|
||||
if fileStatusLimited {
|
||||
fsv.lineLimit(1)
|
||||
} else {
|
||||
fsv
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
fileStatusLimited.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +231,7 @@ struct ChatItemInfoView: View {
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(ci, colorScheme))
|
||||
.background(chatItemFrameColor(ci, theme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
@@ -265,7 +300,7 @@ struct ChatItemInfoView: View {
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, colorScheme))
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if qi.text != "" {
|
||||
@@ -289,10 +324,10 @@ struct ChatItemInfoView: View {
|
||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
||||
}
|
||||
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color {
|
||||
(qi.chatDir?.sent ?? false)
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
@@ -414,7 +449,7 @@ struct ChatItemInfoView: View {
|
||||
.foregroundColor(.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
if let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) {
|
||||
switch status {
|
||||
case .sndRcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
@@ -473,8 +508,12 @@ struct ChatItemInfoView: View {
|
||||
if developerTools {
|
||||
shareText += [
|
||||
String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
|
||||
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id)
|
||||
]
|
||||
if let file = ci.file {
|
||||
shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)]
|
||||
}
|
||||
}
|
||||
if let qi = ci.quotedItem {
|
||||
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
|
||||
|
||||
@@ -14,7 +14,7 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@@ -60,7 +60,16 @@ struct ChatView: View {
|
||||
Divider()
|
||||
}
|
||||
ZStack(alignment: .trailing) {
|
||||
let wallpaperImage = theme.wallpaper.type.image
|
||||
let wallpaperType = theme.wallpaper.type
|
||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
||||
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
|
||||
chatItemsList()
|
||||
.if(wallpaperImage != nil) { view in
|
||||
view.modifier(
|
||||
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
|
||||
)
|
||||
}
|
||||
if let proxy = scrollProxy {
|
||||
floatingButtons(proxy)
|
||||
}
|
||||
@@ -78,7 +87,9 @@ struct ChatView: View {
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.environmentObject(theme)
|
||||
.onAppear {
|
||||
initChatView()
|
||||
}
|
||||
@@ -89,6 +100,7 @@ struct ChatView: View {
|
||||
chat = c
|
||||
}
|
||||
initChatView()
|
||||
theme = buildTheme()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
@@ -131,7 +143,7 @@ struct ChatView: View {
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
connectionCode = nil
|
||||
@@ -384,7 +396,7 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(unreadAbove)
|
||||
.font(.callout)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollUp(proxy) }
|
||||
.contextMenu {
|
||||
@@ -404,13 +416,13 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(counts.unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollToBottom(proxy) }
|
||||
} else if counts.totalBelow > 16 {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollToBottom(proxy) }
|
||||
}
|
||||
@@ -513,7 +525,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
ChatItemWithMenu(
|
||||
chat: chat,
|
||||
@@ -528,7 +540,7 @@ struct ChatView: View {
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat
|
||||
@@ -598,7 +610,7 @@ struct ChatView: View {
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize)
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if chatView.membersLoaded {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
@@ -662,7 +674,8 @@ struct ChatView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
|
||||
.environmentObject(theme)
|
||||
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu, backgroundColor: theme.colors.background)
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
chatItemReactions(ci)
|
||||
@@ -729,7 +742,7 @@ struct ChatView: View {
|
||||
Text("\(r.totalReacted)")
|
||||
.font(.caption)
|
||||
.fontWeight(r.userReacted ? .bold : .light)
|
||||
.foregroundColor(r.userReacted ? .accentColor : .secondary)
|
||||
.foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
@@ -1219,6 +1232,27 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func buildTheme() -> AppTheme {
|
||||
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
|
||||
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
|
||||
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else if case let .group(groupInfo) = chat.chatInfo {
|
||||
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else {
|
||||
nil as ThemeModeOverride?
|
||||
}
|
||||
let overrides = if perChatTheme != nil {
|
||||
ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
} else {
|
||||
nil as ThemeManager.ActiveTheme?
|
||||
}
|
||||
let theme = overrides ?? CurrentColors
|
||||
return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper)
|
||||
} else {
|
||||
return AppTheme.shared
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleNtfsButton: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeFileView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let fileName: String
|
||||
let cancelFile: (() -> Void)
|
||||
let cancelEnabled: Bool
|
||||
@@ -33,7 +33,7 @@ struct ComposeFileView: View {
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.frame(height: 50)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ComposeImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let images: [String]
|
||||
let cancelImage: (() -> Void)
|
||||
let cancelEnabled: Bool
|
||||
@@ -48,7 +48,7 @@ struct ComposeImageView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
}
|
||||
|
||||
struct ComposeLinkView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let linkPreview: LinkPreview?
|
||||
var cancelPreview: (() -> Void)? = nil
|
||||
let cancelEnabled: Bool
|
||||
@@ -62,7 +62,7 @@ struct ComposeLinkView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ enum UploadContent: Equatable {
|
||||
|
||||
struct ComposeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var keyboardVisible: Bool
|
||||
@@ -353,10 +354,10 @@ struct ComposeView: View {
|
||||
keyboardVisible: $keyboardVisible,
|
||||
sendButtonColor: chat.chatInfo.incognito
|
||||
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
||||
: .accentColor
|
||||
: theme.colors.primary
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(.background)
|
||||
.background(theme.colors.background)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if chat.userIsObserver {
|
||||
|
||||
@@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String {
|
||||
|
||||
struct ComposeVoiceView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var recordingFileName: String
|
||||
@Binding var recordingTime: TimeInterval?
|
||||
@Binding var recordingState: VoiceMessageRecordingState
|
||||
@@ -50,7 +50,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.frame(height: ComposeVoiceView.previewHeight)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ struct ComposeVoiceView: View {
|
||||
Button {
|
||||
startPlayback()
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(recordingTime))
|
||||
case .playing:
|
||||
@@ -88,7 +88,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.pause()
|
||||
playbackState = .paused
|
||||
} label: {
|
||||
playPauseIcon("pause.fill")
|
||||
playPauseIcon("pause.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
@@ -96,7 +96,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.play()
|
||||
playbackState = .playing
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
}
|
||||
@@ -131,7 +131,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
|
||||
private func playPauseIcon(_ image: String, _ color: Color) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
@@ -162,6 +162,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
|
||||
private struct ProgressBar: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -169,7 +170,7 @@ struct ComposeVoiceView: View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
.fill(theme.colors.primary)
|
||||
.frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4)
|
||||
.animation(.linear, value: progress)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContextInvitingContactMemberView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -20,7 +20,7 @@ struct ContextInvitingContactMemberView: View {
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
let contextItem: ChatItem
|
||||
let contextIcon: String
|
||||
@@ -44,7 +44,7 @@ struct ContextItemView: View {
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(chatItemFrameColor(contextItem, colorScheme))
|
||||
.background(chatItemFrameColor(contextItem, theme))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField(height: _height)
|
||||
field.backgroundColor = .clear
|
||||
field.text = text
|
||||
field.textAlignment = alignment(text)
|
||||
field.autocapitalizationType = .sentences
|
||||
|
||||
@@ -13,6 +13,7 @@ private let liveMsgInterval: UInt64 = 3000_000000
|
||||
|
||||
struct SendMessageView: View {
|
||||
@Binding var composeState: ComposeState
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var sendMessage: (Int?) -> Void
|
||||
var sendLiveMessage: (() async -> Void)? = nil
|
||||
var updateLiveMessage: (() async -> Void)? = nil
|
||||
@@ -247,6 +248,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
|
||||
private struct RecordVoiceMessageButton: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var startVoiceMessageRecording: (() -> Void)?
|
||||
var finishVoiceMessageRecording: (() -> Void)?
|
||||
@Binding var holdingVMR: Bool
|
||||
@@ -256,7 +258,7 @@ struct SendMessageView: View {
|
||||
var body: some View {
|
||||
Button(action: {}) {
|
||||
Image(systemName: "mic.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(disabled)
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -323,7 +325,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -340,7 +342,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -383,7 +385,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
Task {
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
while composeState.liveMessage != nil {
|
||||
while await composeState.liveMessage != nil {
|
||||
await update()
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
}
|
||||
@@ -394,7 +396,7 @@ struct SendMessageView: View {
|
||||
private func finishVoiceMessageRecordingButton() -> some View {
|
||||
Button(action: { finishVoiceMessageRecording?() }) {
|
||||
Image(systemName: "stop.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.frame(width: 29, height: 29)
|
||||
|
||||
@@ -21,6 +21,7 @@ struct AddGroupMembersView: View {
|
||||
|
||||
struct AddGroupMembersViewCommon: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chat: Chat
|
||||
@State var groupInfo: GroupInfo
|
||||
var creatingGroup: Bool = false
|
||||
@@ -125,6 +126,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
.onChange(of: selectedContacts) { _ in
|
||||
searchFocussed = false
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@@ -176,7 +178,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
} else {
|
||||
if checked {
|
||||
icon = "checkmark.circle.fill"
|
||||
iconColor = .accentColor
|
||||
iconColor = theme.colors.primary
|
||||
} else {
|
||||
icon = "circle"
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
@@ -135,6 +135,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -210,6 +211,7 @@ struct GroupChatInfoView: View {
|
||||
private struct MemberRowView: View {
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var user: Bool = false
|
||||
@Binding var alert: GroupChatInfoViewAlert?
|
||||
|
||||
@@ -282,7 +284,7 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .unblockMemberAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +296,7 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .unblockForAllAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
@@ -333,6 +335,7 @@ struct GroupChatInfoView: View {
|
||||
creatingGroup: false
|
||||
)
|
||||
.navigationBarTitle("Group link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if groupLink == nil {
|
||||
@@ -350,6 +353,7 @@ struct GroupChatInfoView: View {
|
||||
groupProfile: groupInfo.groupProfile
|
||||
)
|
||||
.navigationBarTitle("Group profile")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Edit group profile", systemImage: "pencil")
|
||||
@@ -364,6 +368,7 @@ struct GroupChatInfoView: View {
|
||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||
)
|
||||
.navigationTitle("Welcome message")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
groupInfo.groupProfile.description == nil
|
||||
@@ -518,6 +523,7 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
.navigationBarTitle("Group preferences")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
|
||||
@@ -133,6 +133,7 @@ struct GroupLinkView: View {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func createGroupLink() {
|
||||
|
||||
@@ -35,6 +35,7 @@ struct GroupMemberInfoView: View {
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case planAndConnectAlert(alert: PlanAndConnectAlert)
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: String {
|
||||
@@ -49,6 +50,7 @@ struct GroupMemberInfoView: View {
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
|
||||
case let .queueInfo(info): return "queueInfo \(info)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -178,6 +180,18 @@ struct GroupMemberInfoView: View {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
Button ("Debug delivery") {
|
||||
Task {
|
||||
do {
|
||||
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
|
||||
await MainActor.run { alert = .queueInfo(info: info) }
|
||||
} catch let e {
|
||||
logger.error("apiContactQueueInfo error: \(responseError(e))")
|
||||
let a = getErrorAlert(e, "Error")
|
||||
await MainActor.run { alert = .error(title: a.title, error: a.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +237,7 @@ struct GroupMemberInfoView: View {
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
@@ -232,6 +247,7 @@ struct GroupMemberInfoView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||
@@ -359,6 +375,7 @@ struct GroupMemberInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
member.verified ? "View security code" : "Verify security code",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -66,6 +66,7 @@ struct VerifyCodeView: View {
|
||||
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationTitle("Scan code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label("Scan code", systemImage: "qrcode")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ private let rowHeights: [DynamicTypeSize: CGFloat] = [
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showContactRequestDialog = false
|
||||
@@ -224,7 +225,7 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
}
|
||||
|
||||
@ViewBuilder private func markReadButton() -> some View {
|
||||
@@ -234,14 +235,14 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Read", systemImage: "checkmark")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
} else {
|
||||
Button {
|
||||
Task { await markChatUnread(chat) }
|
||||
} label: {
|
||||
Label("Unread", systemImage: "circlebadge.fill")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -306,7 +307,7 @@ struct ChatListNavLink: View {
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
|
||||
} label: { Label("Accept", systemImage: "checkmark") }
|
||||
.tint(.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
|
||||
} label: {
|
||||
@@ -346,13 +347,16 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Name", systemImage: "pencil")
|
||||
}
|
||||
.tint(.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.sheet(isPresented: $showContactConnectionInfo) {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
Group {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
@@ -467,7 +471,7 @@ struct ChatListNavLink: View {
|
||||
.padding(4)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.sheet(isPresented: $showInvalidJSON) {
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@State private var searchMode = false
|
||||
@FocusState private var searchFocussed
|
||||
@@ -86,6 +87,7 @@ struct ChatListView: View {
|
||||
))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarHidden(searchMode)
|
||||
.toolbar {
|
||||
@@ -136,7 +138,7 @@ struct ChatListView: View {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,12 +156,14 @@ struct ChatListView: View {
|
||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.offset(x: -8)
|
||||
}
|
||||
@@ -179,7 +183,7 @@ struct ChatListView: View {
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
|
||||
private func onboardingButtons() -> some View {
|
||||
@@ -190,7 +194,7 @@ struct ChatListView: View {
|
||||
p.addLine(to: CGPoint(x: 0, y: 10))
|
||||
p.addLine(to: CGPoint(x: 8, y: 0))
|
||||
}
|
||||
.fill(Color.accentColor)
|
||||
.fill(theme.colors.primary)
|
||||
.frame(width: 20, height: 10)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
@@ -213,7 +217,7 @@ struct ChatListView: View {
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.background(Color.accentColor)
|
||||
.background(theme.colors.primary)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
@@ -276,6 +280,7 @@ struct ChatListView: View {
|
||||
|
||||
struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@@ -330,7 +335,7 @@ struct ChatListSearchBar: View {
|
||||
|
||||
if searchFocussed {
|
||||
Text("Cancel")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var progressByTimeout: Bool
|
||||
@State var deleting: Bool = false
|
||||
@@ -94,7 +95,7 @@ struct ChatPreviewView: View {
|
||||
case let .group(groupInfo):
|
||||
let v = previewTitle(t)
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
case .memAccepted: v.foregroundColor(.secondary)
|
||||
default: if deleting { v.foregroundColor(.secondary) } else { v }
|
||||
}
|
||||
@@ -133,7 +134,7 @@ struct ChatPreviewView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
|
||||
.cornerRadius(10)
|
||||
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
@@ -151,7 +152,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> Text {
|
||||
let msg = draft.message
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||
+ attachment()
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
|
||||
|
||||
@@ -206,7 +207,7 @@ struct ChatPreviewView: View {
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
chatPreviewInfoText("Tap to Connect")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
} else if !contact.ready && contact.activeConn != nil {
|
||||
if contact.nextSendGrpInv {
|
||||
chatPreviewInfoText("send direct message")
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ContactConnectionInfo: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var contactConnection: PendingContactConnection
|
||||
@State private var alert: CCInfoAlert?
|
||||
@@ -82,6 +83,7 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
if #available(iOS 16, *) {
|
||||
v
|
||||
} else {
|
||||
@@ -149,7 +151,7 @@ struct ContactConnectionInfo: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Incognito")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
@@ -178,6 +180,7 @@ private func oneTimeLinkLearnMoreButton() -> some View {
|
||||
NavigationLink {
|
||||
AddContactLearnMore(showTitle: false)
|
||||
.navigationTitle("One-time invitation link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("info.circle") {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -23,7 +24,7 @@ struct ContactRequestView: View {
|
||||
Text(contactRequest.chatViewName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.padding(.leading, 8)
|
||||
.frame(alignment: .topLeading)
|
||||
Spacer()
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
|
||||
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
|
||||
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showConnectDesktop: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@@ -21,9 +19,6 @@ struct UserPicker: View {
|
||||
private let menuButtonHeight: CGFloat = 68
|
||||
@State var chatViewNameWidth: CGFloat = 0
|
||||
|
||||
var fillColor: Color {
|
||||
colorScheme == .dark ? fillColorDark : fillColorLight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -82,7 +77,7 @@ struct UserPicker: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(fillColor)
|
||||
.fill(theme.colors.surface)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
|
||||
)
|
||||
@@ -137,7 +132,7 @@ struct UserPicker: View {
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
|
||||
} else if !user.showNtfs {
|
||||
Image(systemName: "speaker.slash")
|
||||
}
|
||||
@@ -145,7 +140,7 @@ struct UserPicker: View {
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
|
||||
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
|
||||
@@ -162,7 +157,7 @@ struct UserPicker: View {
|
||||
.padding(.vertical, 22)
|
||||
.frame(height: menuButtonHeight)
|
||||
}
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ struct DatabaseView: View {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("Database passphrase")
|
||||
}
|
||||
@@ -144,6 +145,7 @@ struct DatabaseView: View {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
@@ -355,6 +357,7 @@ struct DatabaseView: View {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
|
||||
@@ -189,6 +189,7 @@ struct MigrateToAppGroupView: View {
|
||||
Task {
|
||||
do {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.exported) }
|
||||
} catch let error {
|
||||
@@ -231,6 +232,7 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
||||
if !ChatModel.shared.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatInfoImage: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var size: CGFloat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
@@ -24,8 +24,7 @@ struct ChatInfoImage: View {
|
||||
case .contactRequest: iconName = "person.crop.circle.fill"
|
||||
default: iconName = "circle.fill"
|
||||
}
|
||||
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
|
||||
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
|
||||
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
|
||||
return ProfileImage(
|
||||
imageStr: chat.chatInfo.image,
|
||||
iconName: iconName,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// ChatWallpaper.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatViewBackground: ViewModifier {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var image: Image
|
||||
var imageType: WallpaperType
|
||||
var background: Color
|
||||
var tint: Color
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.background(
|
||||
Canvas { context, size in
|
||||
var image = context.resolve(image)
|
||||
image.shading = .color(tint)
|
||||
let rect = CGRectMake(0, 0, size.width, size.height)
|
||||
func repeatDraw(_ imageScale: CGFloat) {
|
||||
let scale = imageScale
|
||||
for h in 0 ... Int(size.height / image.size.height / scale) {
|
||||
for w in 0 ... Int(size.width / image.size.width / scale) {
|
||||
let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale)
|
||||
context.draw(image, in: rect, style: FillStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
context.fill(Path(rect), with: .color(background))
|
||||
switch imageType {
|
||||
case let WallpaperType.Preset(filename, scale): repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1)))
|
||||
case let WallpaperType.Image(_, scale, scaleType):
|
||||
let scaleType = scaleType ?? WallpaperScaleType.fill
|
||||
switch scaleType {
|
||||
case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1))
|
||||
case WallpaperScaleType.fill: fallthrough
|
||||
case WallpaperScaleType.fit:
|
||||
// let scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
// let scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
// let 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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
context.fill(Path(rect), with: .color(tint))
|
||||
}
|
||||
case WallpaperType.Empty: ()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PresetWallpaper {
|
||||
public func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType {
|
||||
WallpaperType.Preset(
|
||||
filename,
|
||||
scale ?? themeOverridesDefault.get().first { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base }?.wallpaper?.scale ?? 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>, backgroundColor: Color) -> some View {
|
||||
Group {
|
||||
if allowMenu.wrappedValue {
|
||||
if hasImageOrVideo {
|
||||
@@ -19,10 +19,10 @@ extension View {
|
||||
self.environmentObject(ChatModel.shared)
|
||||
.overlay(DetermineWidthImageVideoItem())
|
||||
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
|
||||
.frame(maxWidth: itemWidth.wrappedValue)
|
||||
} else {
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
} else {
|
||||
@@ -42,11 +42,14 @@ struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
var maxWidth: CGFloat
|
||||
var itemWidth: Binding<CGFloat>
|
||||
@Binding var menu: UIMenu
|
||||
var backgroundColor: Color
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = HostingViewHolder()
|
||||
view.backgroundColor = .clear
|
||||
let hostView = UIHostingController(rootView: content)
|
||||
// hostView.view.backgroundColor = UIColor(backgroundColor)
|
||||
hostView.view.backgroundColor = .clear
|
||||
view.contentSize = hostView.view.intrinsicContentSize
|
||||
hostView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraints = [
|
||||
@@ -72,6 +75,7 @@ struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
if was != (uiView as! HostingViewHolder).contentSize {
|
||||
uiView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
//uiView.backgroundColor = UIColor(backgroundColor)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
||||
@@ -16,6 +16,7 @@ struct ProfileImage: View {
|
||||
var iconName: String = "person.crop.circle.fill"
|
||||
var size: CGFloat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
var backgroundColor: Color? = nil
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
@@ -28,6 +29,7 @@ struct ProfileImage: View {
|
||||
.resizable()
|
||||
.foregroundColor(color)
|
||||
.frame(width: size, height: size)
|
||||
.background(backgroundColor != nil ? backgroundColor! : .clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ViewModifiers.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 12.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
|
||||
if condition() {
|
||||
transform(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ struct PasscodeEntry: View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: h, height: h)
|
||||
.foregroundColor(Color(uiColor: .systemBackground))
|
||||
.foregroundColor(AppTheme.shared.colors.background)
|
||||
label()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct PasscodeView: View {
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.background(AppTheme.shared.colors.background)
|
||||
}
|
||||
|
||||
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
|
||||
|
||||
@@ -53,6 +53,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
|
||||
struct MigrateFromDevice: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showProgressOnSettings: Bool
|
||||
@@ -215,7 +216,7 @@ struct MigrateFromDevice: View {
|
||||
Section {
|
||||
Button(action: { migrationState = .archiving }) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Archive and upload").foregroundColor(.accentColor)
|
||||
Text("Archive and upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -249,7 +250,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
}
|
||||
let ratio = Float(uploadedBytes) / Float(totalBytes)
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary)
|
||||
}
|
||||
.onAppear {
|
||||
startUploading(totalBytes, archivePath)
|
||||
@@ -263,7 +264,7 @@ struct MigrateFromDevice: View {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Repeat upload").foregroundColor(.accentColor)
|
||||
Text("Repeat upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -299,7 +300,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Finalize migration").foregroundColor(.accentColor)
|
||||
Text("Finalize migration").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
@@ -340,7 +341,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
Button(action: { alert = .deleteChat() }) {
|
||||
settingsRow("trash.fill") {
|
||||
Text("Delete database from this device").foregroundColor(.accentColor)
|
||||
Text("Delete database from this device").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -379,7 +380,7 @@ struct MigrateFromDevice: View {
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
|
||||
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
|
||||
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
Text(description)
|
||||
@@ -389,7 +390,7 @@ struct MigrateFromDevice: View {
|
||||
Text(title)
|
||||
.font(.system(size: 54))
|
||||
.bold()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(primaryColor)
|
||||
|
||||
Text(description)
|
||||
.font(.title3)
|
||||
@@ -398,7 +399,7 @@ struct MigrateFromDevice: View {
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(value))
|
||||
.stroke(
|
||||
Color.accentColor,
|
||||
primaryColor,
|
||||
style: StrokeStyle(lineWidth: 27)
|
||||
)
|
||||
.rotationEffect(.degrees(180))
|
||||
|
||||
@@ -91,6 +91,7 @@ private enum MigrateToDeviceViewAlert: Identifiable {
|
||||
|
||||
struct MigrateToDevice: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var migrationState: MigrationToState?
|
||||
@@ -243,7 +244,7 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
}
|
||||
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +256,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .linkDownloading(link: link)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.down") {
|
||||
Text("Repeat download").foregroundColor(.accentColor)
|
||||
Text("Repeat download").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -293,7 +294,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .archiveImport(archivePath: archivePath)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text("Repeat import").foregroundColor(.accentColor)
|
||||
Text("Repeat import").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -334,7 +335,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text(button).foregroundColor(.accentColor)
|
||||
Text(button).foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -364,6 +365,7 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
|
||||
struct OnionView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var appSettings: AppSettings
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
var finishMigration: (AppSettings) -> Void
|
||||
@@ -381,7 +383,7 @@ struct MigrateToDevice: View {
|
||||
finishMigration(appSettings)
|
||||
}) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Apply").foregroundColor(.accentColor)
|
||||
Text("Apply").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -475,6 +477,7 @@ struct MigrateToDevice: View {
|
||||
chatInitControllerRemovingDatabases()
|
||||
}
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
|
||||
@@ -30,6 +30,7 @@ struct AddContactLearnMore: View {
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct AddGroupView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
@State private var chat: Chat?
|
||||
@@ -95,7 +96,7 @@ struct AddGroupView: View {
|
||||
Section {
|
||||
groupNameTextField()
|
||||
Button(action: createGroup) {
|
||||
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
|
||||
settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") }
|
||||
}
|
||||
.disabled(!canCreateProfile())
|
||||
IncognitoToggle(incognitoEnabled: $incognitoDefault)
|
||||
@@ -144,6 +145,7 @@ struct AddGroupView: View {
|
||||
profile.image = nil
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func groupNameTextField() -> some View {
|
||||
|
||||
@@ -94,7 +94,7 @@ struct NewChatView: View {
|
||||
.background(
|
||||
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
|
||||
Rectangle()
|
||||
.fill(Color(uiColor: .systemGroupedBackground))
|
||||
.fill(AppTheme.shared.colors.background)
|
||||
)
|
||||
.animation(.easeInOut(duration: 0.3333), value: selection)
|
||||
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
|
||||
@@ -113,7 +113,7 @@ struct NewChatView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
.onChange(of: invitationUsed) { used in
|
||||
if used && !(m.showingInvitation?.connChatUsed ?? true) {
|
||||
m.markShowingInvitationUsed()
|
||||
@@ -367,11 +367,12 @@ struct ScannerInView: View {
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
var scanMode: ScanMode = .continuous
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -436,6 +437,7 @@ struct ScannerInView: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func linkTextView(_ link: String) -> some View {
|
||||
Text(link)
|
||||
.lineLimit(1)
|
||||
@@ -486,6 +488,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
|
||||
}
|
||||
|
||||
struct IncognitoToggle: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var incognitoEnabled: Bool
|
||||
@State private var showIncognitoSheet = false
|
||||
|
||||
@@ -499,7 +502,7 @@ struct IncognitoToggle: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Incognito")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
|
||||
@@ -66,6 +66,7 @@ struct CreateProfile: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Create your profile")
|
||||
.modifier(ThemedBackground())
|
||||
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
|
||||
.onAppear() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
@@ -78,6 +79,7 @@ struct CreateProfile: View {
|
||||
|
||||
struct CreateFirstProfile: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var displayName: String = ""
|
||||
@FocusState private var focusDisplayName
|
||||
@@ -89,9 +91,9 @@ struct CreateFirstProfile: View {
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct HowItWorks: View {
|
||||
.lineLimit(10)
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ struct SetNotificationsMode: View {
|
||||
}
|
||||
|
||||
struct NtfModeSelector: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var mode: NotificationsMode
|
||||
@Binding var selection: NotificationsMode
|
||||
@State private var tapped = false
|
||||
@@ -87,7 +88,7 @@ struct NtfModeSelector: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(mode.label)
|
||||
.font(.headline)
|
||||
.foregroundColor(selection == mode ? .accentColor : .secondary)
|
||||
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||
Text(ntfModeDescription(mode))
|
||||
.lineLimit(10)
|
||||
.font(.subheadline)
|
||||
@@ -95,11 +96,11 @@ struct NtfModeSelector: View {
|
||||
.padding(12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
|
||||
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
.stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
._onButtonGesture { down in
|
||||
tapped = down
|
||||
|
||||
@@ -79,7 +79,7 @@ struct SimpleXInfo: View {
|
||||
MigrateToDevice(migrationState: $m.migrationState)
|
||||
}
|
||||
.navigationTitle("Migrate here")
|
||||
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHowItWorks) {
|
||||
|
||||
@@ -406,7 +406,28 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
description: "More reliable network connection."
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version: "v5.8",
|
||||
post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"),
|
||||
features: [
|
||||
FeatureDescription(
|
||||
icon: "arrow.forward",
|
||||
title: "Private message routing 🚀",
|
||||
description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "network.badge.shield.half.filled",
|
||||
title: "Safely receive files",
|
||||
description: "Confirm files from unknown servers."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "battery.50",
|
||||
title: "Improved message delivery",
|
||||
description: "With reduced battery usage."
|
||||
)
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
private let lastVersion = versionDescriptions.last!.version
|
||||
|
||||
@@ -178,26 +178,32 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connect to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
||||
List {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
ZStack {
|
||||
List {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func searchingDesktopView() -> some View {
|
||||
@@ -215,6 +221,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
|
||||
@@ -242,6 +249,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Found desktop")
|
||||
.modifier(ThemedBackground())
|
||||
|
||||
if compatible && connectRemoteViaMulticastAuto {
|
||||
v.onAppear { confirmKnownDesktop(rc) }
|
||||
@@ -271,6 +279,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verify connection")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||
@@ -311,6 +320,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connected to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func sessionCodeText(_ code: String) -> some View {
|
||||
@@ -332,13 +342,7 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func scanDesctopAddressView() -> some View {
|
||||
Section("Scan QR code from desktop") {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.padding(.horizontal)
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +395,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Linked desktops")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
||||
|
||||
@@ -106,6 +106,7 @@ struct TerminalView: View {
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationTitle("Chat console")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
|
||||
@@ -130,6 +131,7 @@ struct TerminalView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear { terminalItem = nil }
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func consoleSendMessage() {
|
||||
|
||||
@@ -44,6 +44,11 @@ extension AppSettings {
|
||||
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
|
||||
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
|
||||
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
|
||||
if let val = uiProfileImageCornerRadius { def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) }
|
||||
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
|
||||
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
|
||||
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
|
||||
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
|
||||
}
|
||||
|
||||
public static var current: AppSettings {
|
||||
@@ -69,6 +74,11 @@ extension AppSettings {
|
||||
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
||||
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
||||
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||
c.uiProfileImageCornerRadius = def.float(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
c.uiColorScheme = currentThemeDefault.get()
|
||||
c.uiDarkColorScheme = systemDarkThemeDefault.get()
|
||||
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
|
||||
c.uiThemes = themeOverridesDefault.get()
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
|
||||
let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
|
||||
//let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
|
||||
|
||||
let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"]
|
||||
let colorModesLocalized: [LocalizedStringKey] = ["System", "Light", "Dark"]
|
||||
let colorModesNames: [DefaultThemeMode?] = [nil, DefaultThemeMode.light, DefaultThemeMode.dark]
|
||||
|
||||
let darkThemesLocalized: [LocalizedStringKey] = ["Dark", "SimpleX", "Black"]
|
||||
let darkThemesNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName, DefaultTheme.BLACK.themeName]
|
||||
|
||||
let appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
|
||||
|
||||
@@ -22,7 +27,11 @@ struct AppearanceSettings: View {
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@State private var iconLightTapped = false
|
||||
@State private var iconDarkTapped = false
|
||||
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
//@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
@State private var colorMode: DefaultThemeMode? = {
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { nil as DefaultThemeMode? } else { CurrentColors.base.mode }
|
||||
}()
|
||||
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.SIMPLEX.themeName
|
||||
@State private var uiTintColor = getUIAccentColorDefault()
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||
|
||||
@@ -65,9 +74,15 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Theme", selection: $userInterfaceStyle) {
|
||||
ForEach(interfaceStyles, id: \.self) { style in
|
||||
Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0])
|
||||
Picker("Color mode", selection: $colorMode) {
|
||||
ForEach(Array(colorModesNames.enumerated()), id: \.element) { index, mode in
|
||||
Text(colorModesLocalized[index])
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
Picker("Dark mode colors", selection: $darkModeTheme) {
|
||||
ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in
|
||||
Text(darkThemesLocalized[index])
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
@@ -82,9 +97,24 @@ struct AppearanceSettings: View {
|
||||
Text("Reset colors").font(.callout)
|
||||
}
|
||||
}
|
||||
.onChange(of: userInterfaceStyle) { _ in
|
||||
sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
setUserInterfaceStyleDefault(userInterfaceStyle)
|
||||
.onChange(of: colorMode) { mode in
|
||||
guard let mode else {
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
return
|
||||
}
|
||||
if case DefaultThemeMode.light = mode {
|
||||
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
|
||||
} else if case DefaultThemeMode.dark = mode {
|
||||
ThemeManager.applyTheme(systemDarkThemeDefault.get())
|
||||
}
|
||||
}
|
||||
.onChange(of: darkModeTheme) { darkTheme in
|
||||
ThemeManager.changeDarkTheme(darkTheme)
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME {
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
} else if currentThemeDefault.get() != DefaultTheme.LIGHT.themeName {
|
||||
ThemeManager.applyTheme(systemDarkThemeDefault.get())
|
||||
}
|
||||
}
|
||||
.onChange(of: uiTintColor) { _ in
|
||||
sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor)
|
||||
@@ -136,24 +166,24 @@ func setUIAccentColorDefault(_ color: CGColor) {
|
||||
}
|
||||
}
|
||||
|
||||
func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
|
||||
switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
|
||||
case 1: return .light
|
||||
case 2: return .dark
|
||||
default: return .unspecified
|
||||
}
|
||||
}
|
||||
|
||||
func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
|
||||
var v: Int
|
||||
switch style {
|
||||
case .unspecified: v = 0
|
||||
case .light: v = 1
|
||||
case .dark: v = 2
|
||||
default: v = 0
|
||||
}
|
||||
UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
|
||||
}
|
||||
//func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
|
||||
// switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
|
||||
// case 1: return .light
|
||||
// case 2: return .dark
|
||||
// default: return .unspecified
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
|
||||
// var v: Int
|
||||
// switch style {
|
||||
// case .unspecified: v = 0
|
||||
// case .light: v = 1
|
||||
// case .dark: v = 2
|
||||
// default: v = 0
|
||||
// }
|
||||
// UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
|
||||
//}
|
||||
|
||||
struct AppearanceSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
@@ -22,6 +22,7 @@ struct CallSettings: View {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ struct HiddenProfileView: View {
|
||||
message: Text(savePasswordError ?? "")
|
||||
)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
|
||||
|
||||
@@ -27,6 +27,7 @@ struct IncognitoHelp: View {
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("SMP servers")
|
||||
}
|
||||
@@ -54,6 +55,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("XFTP servers")
|
||||
}
|
||||
@@ -73,6 +75,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
AdvancedNetworkSettings()
|
||||
.navigationTitle("Network settings")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("Advanced network settings")
|
||||
}
|
||||
@@ -82,34 +85,35 @@ 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 {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Send notifications")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $showAlert) { alert in
|
||||
if let token = m.deviceToken {
|
||||
@@ -68,6 +69,7 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Show preview")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
HStack {
|
||||
@@ -172,6 +174,7 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
||||
}
|
||||
|
||||
struct SelectionListView<Item: SelectableItem>: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var list: [Item]
|
||||
@Binding var selection: Item
|
||||
var onSelection: ((Item) -> Void)?
|
||||
@@ -185,11 +188,11 @@ struct SelectionListView<Item: SelectableItem>: View {
|
||||
if selection == item {
|
||||
Image(systemName: "checkmark")
|
||||
.resizable().scaledToFit().frame(width: 16)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
|
||||
.listRowBackground(tapped == item ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||
.onTapGesture {
|
||||
if selection == item { return }
|
||||
if let f = onSelection {
|
||||
|
||||
@@ -48,6 +48,7 @@ struct PrivacySettings: View {
|
||||
NavigationLink {
|
||||
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
|
||||
.navigationTitle("SimpleX Lock")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
if prefPerformLA {
|
||||
settingsRow("lock.fill", color: .green) {
|
||||
@@ -332,6 +333,7 @@ struct SimplexLockView: View {
|
||||
@Binding var prefPerformLA: Bool
|
||||
@Binding var currentLAMode: LAMode
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
|
||||
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
|
||||
@@ -416,7 +418,7 @@ struct SimplexLockView: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Enable self-destruct")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
|
||||
@@ -94,6 +94,7 @@ struct ProtocolServersView: View {
|
||||
}
|
||||
.sheet(isPresented: $showScanProtoServer) {
|
||||
ScanProtocolServer(servers: $servers)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if saveDisabled {
|
||||
@@ -171,6 +172,7 @@ struct ProtocolServersView: View {
|
||||
serverToEdit: srv
|
||||
)
|
||||
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
let address = parseServerAddress(srv.server)
|
||||
|
||||
@@ -89,7 +89,7 @@ struct SetDeliveryReceiptsView: View {
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.background(AppTheme.shared.colors.background)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed"
|
||||
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
|
||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
||||
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||
//let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||
@@ -62,6 +62,11 @@ let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
let DEFAULT_CURRENT_THEME = "currentTheme"
|
||||
let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
|
||||
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
|
||||
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
@@ -88,7 +93,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
//DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
@@ -101,7 +106,12 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue,
|
||||
|
||||
DEFAULT_THEME_OVERRIDES: "{}",
|
||||
DEFAULT_CURRENT_THEME: DefaultTheme.SYSTEM_THEME_NAME,
|
||||
DEFAULT_SYSTEM_DARK_THEME: DefaultTheme.SIMPLEX.themeName,
|
||||
DEFAULT_CURRENT_THEME_IDS: "{}"
|
||||
]
|
||||
|
||||
// not used anymore
|
||||
@@ -148,14 +158,66 @@ let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults
|
||||
|
||||
let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME)
|
||||
|
||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.SIMPLEX.themeName)
|
||||
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
||||
let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_THEME_OVERRIDES, withDefault: [])
|
||||
|
||||
func setGroupDefaults() {
|
||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||
}
|
||||
|
||||
public class StringDefault {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
var defaultValue: String
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: String) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
self.defaultValue = withDefault
|
||||
}
|
||||
|
||||
public func get() -> String {
|
||||
defaults.string(forKey: key) ?? defaultValue
|
||||
}
|
||||
|
||||
public func set(_ value: String) {
|
||||
defaults.set(value, forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
public class CodableDefault<T: Codable> {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
var defaultValue: T
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
self.defaultValue = withDefault
|
||||
}
|
||||
|
||||
public func get() -> T {
|
||||
if let value = defaults.string(forKey: key) {
|
||||
return decodeJSON(value) ?? defaultValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
public func set(_ value: T) {
|
||||
defaults.set(encodeJSON(value), forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@State private var showProgress: Bool = false
|
||||
|
||||
@@ -180,6 +242,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
UserProfile()
|
||||
.navigationTitle("Your current profile")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
ProfilePreview(profileOf: user)
|
||||
.padding(.leading, -8)
|
||||
@@ -197,6 +260,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
UserAddressView(shareViaProfile: user.addressShared)
|
||||
.navigationTitle("SimpleX address")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("qrcode") { Text("Your SimpleX address") }
|
||||
@@ -205,6 +269,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
|
||||
.navigationTitle("Your preferences")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("switch.2") { Text("Chat preferences") }
|
||||
}
|
||||
@@ -219,6 +284,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
|
||||
.navigationTitle("Migrate device")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
|
||||
@@ -230,6 +296,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
NotificationsView()
|
||||
.navigationTitle("Notifications")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
HStack {
|
||||
notificationsIcon()
|
||||
@@ -241,6 +308,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
NetworkAndServers()
|
||||
.navigationTitle("Network & servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("externaldrive.connected.to.line.below") { Text("Network & servers") }
|
||||
}
|
||||
@@ -249,6 +317,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
CallSettings()
|
||||
.navigationTitle("Your calls")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("video") { Text("Audio & video calls") }
|
||||
}
|
||||
@@ -257,6 +326,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
PrivacySettings()
|
||||
.navigationTitle("Your privacy")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("lock") { Text("Privacy & security") }
|
||||
}
|
||||
@@ -266,6 +336,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
AppearanceSettings()
|
||||
.navigationTitle("Appearance")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("sun.max") { Text("Appearance") }
|
||||
}
|
||||
@@ -280,6 +351,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
ChatHelp(showSettings: $showSettings)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("questionmark") { Text("How to use it") }
|
||||
@@ -287,6 +359,7 @@ struct SettingsView: View {
|
||||
}
|
||||
NavigationLink {
|
||||
WhatsNewView(viaSettings: true)
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("plus") { Text("What's new") }
|
||||
@@ -294,6 +367,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
SimpleXInfo(onboarding: false)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("info") { Text("About SimpleX Chat") }
|
||||
@@ -333,18 +407,21 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
DeveloperView()
|
||||
.navigationTitle("Developer tools")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") }
|
||||
}
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
.onDisappear {
|
||||
chatModel.showingTerminal = false
|
||||
@@ -356,6 +433,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
|
||||
.navigationTitle("Your chat database")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary
|
||||
settingsRow("internaldrive", color: color) {
|
||||
|
||||
@@ -300,6 +300,7 @@ struct UserAddressView: View {
|
||||
NavigationLink {
|
||||
UserAddressLearnMore()
|
||||
.navigationTitle("SimpleX address")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("info.circle") {
|
||||
|
||||
@@ -8,6 +8,7 @@ import SimpleXChat
|
||||
|
||||
struct UserProfilesView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@EnvironmentObject private var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@Environment(\.editMode) private var editMode
|
||||
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
|
||||
@@ -111,6 +112,7 @@ struct UserProfilesView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Your chat profiles")
|
||||
.modifier(ThemedBackground())
|
||||
.searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
@@ -123,7 +125,7 @@ struct UserProfilesView: View {
|
||||
deleteModeButton("Profile and server connections", true)
|
||||
deleteModeButton("Local profile data only", false)
|
||||
}
|
||||
.sheet(item: $selectedUser) { user in
|
||||
.appSheet(item: $selectedUser) { user in
|
||||
HiddenProfileView(user: user, profileHidden: $profileHidden)
|
||||
}
|
||||
.onChange(of: profileHidden) { _ in
|
||||
@@ -131,7 +133,7 @@ struct UserProfilesView: View {
|
||||
withAnimation { profileHidden = false }
|
||||
}
|
||||
}
|
||||
.sheet(item: $profileAction) { action in
|
||||
.appSheet(item: $profileAction) { action in
|
||||
profileActionView(action)
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
@@ -237,6 +239,7 @@ struct UserProfilesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder func actionHeader(_ title: LocalizedStringKey, _ user: User) -> some View {
|
||||
@@ -356,7 +359,7 @@ struct UserProfilesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
if #available(iOS 16, *) {
|
||||
|
||||
@@ -713,6 +713,10 @@
|
||||
<target>Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow downgrade" xml:space="preserve">
|
||||
<source>Allow downgrade</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
|
||||
<target>Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)</target>
|
||||
@@ -808,6 +812,10 @@
|
||||
<target>Вече се присъединихте към групата!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use private routing." xml:space="preserve">
|
||||
<source>Always use private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use relay" xml:space="preserve">
|
||||
<source>Always use relay</source>
|
||||
<target>Винаги използвай реле</target>
|
||||
@@ -1088,6 +1096,10 @@
|
||||
<target>Файлът не може да бъде получен</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
|
||||
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<target>Мобилна мрежа</target>
|
||||
@@ -1284,6 +1296,10 @@
|
||||
<target>Потвърди актуализаациите на базата данни</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm files from unknown servers." xml:space="preserve">
|
||||
<source>Confirm files from unknown servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<target>Потвърди мрежовите настройки</target>
|
||||
@@ -1701,6 +1717,10 @@ This is your own one-time link!</source>
|
||||
<target>Базата данни ще бъде мигрирана, когато приложението се рестартира</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Debug delivery" xml:space="preserve">
|
||||
<source>Debug delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Decentralized" xml:space="preserve">
|
||||
<source>Decentralized</source>
|
||||
<target>Децентрализиран</target>
|
||||
@@ -1948,6 +1968,10 @@ This cannot be undone!</source>
|
||||
<target>Настолни устройства</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server error: %@" xml:space="preserve">
|
||||
<source>Destination server error: %@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Develop" xml:space="preserve">
|
||||
<source>Develop</source>
|
||||
<target>Разработване</target>
|
||||
@@ -2053,11 +2077,19 @@ This cannot be undone!</source>
|
||||
<target>Откриване през локалната мрежа</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Do NOT send messages directly, even if your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
|
||||
<source>Do NOT use SimpleX for emergency calls.</source>
|
||||
<target>НЕ използвайте SimpleX за спешни повиквания.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use private routing." xml:space="preserve">
|
||||
<source>Do NOT use private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do it later" xml:space="preserve">
|
||||
<source>Do it later</source>
|
||||
<target>Отложи</target>
|
||||
@@ -2621,7 +2653,7 @@ This cannot be undone!</source>
|
||||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Грешка: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: URL is invalid" xml:space="preserve">
|
||||
<source>Error: URL is invalid</source>
|
||||
@@ -2713,6 +2745,10 @@ This cannot be undone!</source>
|
||||
<target>Файл: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files" xml:space="preserve">
|
||||
<source>Files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files & media" xml:space="preserve">
|
||||
<source>Files & media</source>
|
||||
<target>Файлове и медия</target>
|
||||
@@ -2818,6 +2854,16 @@ This cannot be undone!</source>
|
||||
<target>Препратено от</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve">
|
||||
<source>Forwarding server: %1$@
|
||||
Destination server error: %2$@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve">
|
||||
<source>Forwarding server: %1$@
|
||||
Error: %2$@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<target>Намерено настолно устройство</target>
|
||||
@@ -3633,11 +3679,19 @@ This is your link for group %@!</source>
|
||||
<target>Потвърждениe за доставка на съобщения!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message delivery warning" xml:space="preserve">
|
||||
<source>Message delivery warning</source>
|
||||
<note>item status text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message draft" xml:space="preserve">
|
||||
<source>Message draft</source>
|
||||
<target>Чернова на съобщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message queue info" xml:space="preserve">
|
||||
<source>Message queue info</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions" xml:space="preserve">
|
||||
<source>Message reactions</source>
|
||||
<target>Реакции на съобщения</target>
|
||||
@@ -3653,6 +3707,14 @@ This is your link for group %@!</source>
|
||||
<target>Реакциите на съобщения са забранени в тази група.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message routing fallback" xml:space="preserve">
|
||||
<source>Message routing fallback</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message routing mode" xml:space="preserve">
|
||||
<source>Message routing mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Източникът на съобщението остава скрит.</target>
|
||||
@@ -3783,11 +3845,6 @@ This is your link for group %@!</source>
|
||||
<target>Най-вероятно тази връзка е изтрита.</target>
|
||||
<note>item status description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
|
||||
<source>Most likely this contact has deleted the connection with you.</source>
|
||||
<target>Най-вероятно този контакт е изтрил връзката с вас.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Multiple chat profiles" xml:space="preserve">
|
||||
<source>Multiple chat profiles</source>
|
||||
<target>Множество профили за чат</target>
|
||||
@@ -3818,6 +3875,10 @@ This is your link for group %@!</source>
|
||||
<target>Мрежова връзка</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve">
|
||||
<source>Network issues - message expired after many attempts to send it.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network management" xml:space="preserve">
|
||||
<source>Network management</source>
|
||||
<target>Управление на мрежата</target>
|
||||
@@ -4359,11 +4420,23 @@ Error: %@</source>
|
||||
<target>Поверителни имена на файлове</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private message routing" xml:space="preserve">
|
||||
<source>Private message routing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private message routing 🚀" xml:space="preserve">
|
||||
<source>Private message routing 🚀</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private notes" xml:space="preserve">
|
||||
<source>Private notes</source>
|
||||
<target>Лични бележки</target>
|
||||
<note>name of notes to self</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private routing" xml:space="preserve">
|
||||
<source>Private routing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile and server connections" xml:space="preserve">
|
||||
<source>Profile and server connections</source>
|
||||
<target>Профилни и сървърни връзки</target>
|
||||
@@ -4444,11 +4517,20 @@ Error: %@</source>
|
||||
<target>Забрани изпращането на гласови съобщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect IP address" xml:space="preserve">
|
||||
<source>Protect IP address</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect app screen" xml:space="preserve">
|
||||
<source>Protect app screen</source>
|
||||
<target>Защити екрана на приложението</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve">
|
||||
<source>Protect your IP address from the messaging relays chosen by your contacts.
|
||||
Enable in *Network & servers* settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect your chat profiles with a password!" xml:space="preserve">
|
||||
<source>Protect your chat profiles with a password!</source>
|
||||
<target>Защитете чат профилите с парола!</target>
|
||||
@@ -4554,11 +4636,6 @@ Error: %@</source>
|
||||
<target>Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<target>Паралелност на получаване</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>Получаващият се файл ще бъде спрян.</target>
|
||||
@@ -4794,6 +4871,10 @@ Error: %@</source>
|
||||
<target>SMP сървъри</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safely receive files" xml:space="preserve">
|
||||
<source>Safely receive files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<target>По-безопасни групи</target>
|
||||
@@ -5019,6 +5100,14 @@ Error: %@</source>
|
||||
<target>Изпрати съобщение на живо</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Send messages directly when your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send notifications" xml:space="preserve">
|
||||
<source>Send notifications</source>
|
||||
<target>Изпращай известия</target>
|
||||
@@ -5124,6 +5213,10 @@ Error: %@</source>
|
||||
<target>Изпратените съобщения ще бъдат изтрити след зададеното време.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server address is incompatible with network settings." xml:space="preserve">
|
||||
<source>Server address is incompatible with network settings.</source>
|
||||
<note>srv error text.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
|
||||
<source>Server requires authorization to create queues, check password</source>
|
||||
<target>Сървърът изисква оторизация за създаване на опашки, проверете паролата</target>
|
||||
@@ -5139,6 +5232,10 @@ Error: %@</source>
|
||||
<target>Тестът на сървъра е неуспешен!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server version is incompatible with network settings." xml:space="preserve">
|
||||
<source>Server version is incompatible with network settings.</source>
|
||||
<note>srv error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Servers" xml:space="preserve">
|
||||
<source>Servers</source>
|
||||
<target>Сървъри</target>
|
||||
@@ -5259,11 +5356,19 @@ Error: %@</source>
|
||||
<target>Показване на последните съобщения в листа с чатовете</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show message status" xml:space="preserve">
|
||||
<source>Show message status</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show preview" xml:space="preserve">
|
||||
<source>Show preview</source>
|
||||
<target>Показване на визуализация</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show → on messages sent via private routing." xml:space="preserve">
|
||||
<source>Show → on messages sent via private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show:" xml:space="preserve">
|
||||
<source>Show:</source>
|
||||
<target>Покажи:</target>
|
||||
@@ -5586,6 +5691,10 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve">
|
||||
<source>The app will ask to confirm downloads from unknown file servers (except .onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve">
|
||||
<source>The attempt to change database passphrase was not completed.</source>
|
||||
<target>Опитът за промяна на паролата на базата данни не беше завършен.</target>
|
||||
@@ -5771,6 +5880,10 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>За да не се разкрива часовата зона, файловете с изображения/глас използват UTC.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve">
|
||||
<source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve">
|
||||
<source>To protect your information, turn on SimpleX Lock.
|
||||
You will be prompted to complete authentication before this feature is enabled.</source>
|
||||
@@ -5863,11 +5976,6 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Отблокирай член?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unexpected error: %@" xml:space="preserve">
|
||||
<source>Unexpected error: %@</source>
|
||||
<target>Неочаквана грешка: %@</target>
|
||||
<note>item status description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unexpected migration state" xml:space="preserve">
|
||||
<source>Unexpected migration state</source>
|
||||
<target>Неочаквано състояние на миграция</target>
|
||||
@@ -5913,6 +6021,10 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Непозната грешка</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown servers!" xml:space="preserve">
|
||||
<source>Unknown servers!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
|
||||
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
|
||||
<target>Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания.</target>
|
||||
@@ -6060,6 +6172,14 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Използвай само локални известия?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve">
|
||||
<source>Use private routing with unknown servers when IP address is not protected.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use private routing with unknown servers." xml:space="preserve">
|
||||
<source>Use private routing with unknown servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use server" xml:space="preserve">
|
||||
<source>Use server</source>
|
||||
<target>Използвай сървър</target>
|
||||
@@ -6295,11 +6415,23 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>С намален разход на батерията.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve">
|
||||
<source>Without Tor or VPN, your IP address will be visible to file servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
|
||||
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||
<source>Wrong database passphrase</source>
|
||||
<target>Грешна парола за базата данни</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve">
|
||||
<source>Wrong key or unknown connection - most likely this connection is deleted.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong passphrase!" xml:space="preserve">
|
||||
<source>Wrong passphrase!</source>
|
||||
<target>Грешна парола!</target>
|
||||
@@ -7415,6 +7547,12 @@ SimpleX сървърите не могат да видят вашия профи
|
||||
<target>изпрати лично съобщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve">
|
||||
<source>server queue info: %1$@
|
||||
|
||||
last received msg: %2$@</source>
|
||||
<note>queue info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="set new contact address" xml:space="preserve">
|
||||
<source>set new contact address</source>
|
||||
<target>зададен нов адрес за контакт</target>
|
||||
@@ -7455,11 +7593,19 @@ SimpleX сървърите не могат да видят вашия профи
|
||||
<target>неизвестен</target>
|
||||
<note>connection info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unknown relays" xml:space="preserve">
|
||||
<source>unknown relays</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unknown status" xml:space="preserve">
|
||||
<source>unknown status</source>
|
||||
<target>неизвестен статус</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unprotected" xml:space="preserve">
|
||||
<source>unprotected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="updated group profile" xml:space="preserve">
|
||||
<source>updated group profile</source>
|
||||
<target>актуализиран профил на групата</target>
|
||||
@@ -7525,6 +7671,10 @@ SimpleX сървърите не могат да видят вашия профи
|
||||
<target>седмици</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="when IP hidden" xml:space="preserve">
|
||||
<source>when IP hidden</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="yes" xml:space="preserve">
|
||||
<source>yes</source>
|
||||
<target>да</target>
|
||||
|
||||
@@ -690,6 +690,10 @@
|
||||
<target>Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow downgrade" xml:space="preserve">
|
||||
<source>Allow downgrade</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
|
||||
<target>Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin)</target>
|
||||
@@ -782,6 +786,10 @@
|
||||
<source>Already joining the group!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use private routing." xml:space="preserve">
|
||||
<source>Always use private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use relay" xml:space="preserve">
|
||||
<source>Always use relay</source>
|
||||
<target>Spojení přes relé</target>
|
||||
@@ -1047,6 +1055,10 @@
|
||||
<target>Nelze přijmout soubor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
|
||||
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1238,6 +1250,10 @@
|
||||
<target>Potvrdit aktualizaci databáze</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm files from unknown servers." xml:space="preserve">
|
||||
<source>Confirm files from unknown servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1631,6 +1647,10 @@ This is your own one-time link!</source>
|
||||
<target>Databáze bude přenesena po restartu aplikace</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Debug delivery" xml:space="preserve">
|
||||
<source>Debug delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Decentralized" xml:space="preserve">
|
||||
<source>Decentralized</source>
|
||||
<target>Decentralizované</target>
|
||||
@@ -1870,6 +1890,10 @@ This cannot be undone!</source>
|
||||
<source>Desktop devices</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server error: %@" xml:space="preserve">
|
||||
<source>Destination server error: %@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Develop" xml:space="preserve">
|
||||
<source>Develop</source>
|
||||
<target>Vyvinout</target>
|
||||
@@ -1973,11 +1997,19 @@ This cannot be undone!</source>
|
||||
<source>Discover via local network</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Do NOT send messages directly, even if your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
|
||||
<source>Do NOT use SimpleX for emergency calls.</source>
|
||||
<target>NEpoužívejte SimpleX pro tísňová volání.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use private routing." xml:space="preserve">
|
||||
<source>Do NOT use private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do it later" xml:space="preserve">
|
||||
<source>Do it later</source>
|
||||
<target>Udělat později</target>
|
||||
@@ -2519,7 +2551,7 @@ This cannot be undone!</source>
|
||||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Chyba: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: URL is invalid" xml:space="preserve">
|
||||
<source>Error: URL is invalid</source>
|
||||
@@ -2608,6 +2640,10 @@ This cannot be undone!</source>
|
||||
<target>Soubor: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files" xml:space="preserve">
|
||||
<source>Files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files & media" xml:space="preserve">
|
||||
<source>Files & media</source>
|
||||
<target>Soubory a média</target>
|
||||
@@ -2706,6 +2742,16 @@ This cannot be undone!</source>
|
||||
<source>Forwarded from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve">
|
||||
<source>Forwarding server: %1$@
|
||||
Destination server error: %2$@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve">
|
||||
<source>Forwarding server: %1$@
|
||||
Error: %2$@</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3490,11 +3536,19 @@ This is your link for group %@!</source>
|
||||
<target>Potvrzení o doručení zprávy!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message delivery warning" xml:space="preserve">
|
||||
<source>Message delivery warning</source>
|
||||
<note>item status text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message draft" xml:space="preserve">
|
||||
<source>Message draft</source>
|
||||
<target>Návrh zprávy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message queue info" xml:space="preserve">
|
||||
<source>Message queue info</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions" xml:space="preserve">
|
||||
<source>Message reactions</source>
|
||||
<target>Reakce na zprávy</target>
|
||||
@@ -3510,6 +3564,14 @@ This is your link for group %@!</source>
|
||||
<target>Reakce na zprávy jsou v této skupině zakázány.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message routing fallback" xml:space="preserve">
|
||||
<source>Message routing fallback</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message routing mode" xml:space="preserve">
|
||||
<source>Message routing mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3627,11 +3689,6 @@ This is your link for group %@!</source>
|
||||
<target>Pravděpodobně je toto spojení smazáno.</target>
|
||||
<note>item status description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve">
|
||||
<source>Most likely this contact has deleted the connection with you.</source>
|
||||
<target>Tento kontakt s největší pravděpodobností smazal spojení s vámi.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Multiple chat profiles" xml:space="preserve">
|
||||
<source>Multiple chat profiles</source>
|
||||
<target>Více chatovacích profilů</target>
|
||||
@@ -3661,6 +3718,10 @@ This is your link for group %@!</source>
|
||||
<source>Network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve">
|
||||
<source>Network issues - message expired after many attempts to send it.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network management" xml:space="preserve">
|
||||
<source>Network management</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4181,10 +4242,22 @@ Error: %@</source>
|
||||
<target>Soukromé názvy souborů</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private message routing" xml:space="preserve">
|
||||
<source>Private message routing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private message routing 🚀" xml:space="preserve">
|
||||
<source>Private message routing 🚀</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private notes" xml:space="preserve">
|
||||
<source>Private notes</source>
|
||||
<note>name of notes to self</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private routing" xml:space="preserve">
|
||||
<source>Private routing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile and server connections" xml:space="preserve">
|
||||
<source>Profile and server connections</source>
|
||||
<target>Profil a připojení k serveru</target>
|
||||
@@ -4261,11 +4334,20 @@ Error: %@</source>
|
||||
<target>Zakázat odesílání hlasových zpráv.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect IP address" xml:space="preserve">
|
||||
<source>Protect IP address</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect app screen" xml:space="preserve">
|
||||
<source>Protect app screen</source>
|
||||
<target>Ochrana obrazovky aplikace</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve">
|
||||
<source>Protect your IP address from the messaging relays chosen by your contacts.
|
||||
Enable in *Network & servers* settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Protect your chat profiles with a password!" xml:space="preserve">
|
||||
<source>Protect your chat profiles with a password!</source>
|
||||
<target>Chraňte své chat profily heslem!</target>
|
||||
@@ -4368,10 +4450,6 @@ Error: %@</source>
|
||||
<target>Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>Příjem souboru bude zastaven.</target>
|
||||
@@ -4599,6 +4677,10 @@ Error: %@</source>
|
||||
<target>SMP servery</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safely receive files" xml:space="preserve">
|
||||
<source>Safely receive files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4817,6 +4899,14 @@ Error: %@</source>
|
||||
<target>Odeslat živou zprávu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve">
|
||||
<source>Send messages directly when your or destination server does not support private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send notifications" xml:space="preserve">
|
||||
<source>Send notifications</source>
|
||||
<target>Odeslat oznámení</target>
|
||||
@@ -4921,6 +5011,10 @@ Error: %@</source>
|
||||
<target>Odeslané zprávy se po uplynutí nastavené doby odstraní.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server address is incompatible with network settings." xml:space="preserve">
|
||||
<source>Server address is incompatible with network settings.</source>
|
||||
<note>srv error text.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
|
||||
<source>Server requires authorization to create queues, check password</source>
|
||||
<target>Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</target>
|
||||
@@ -4936,6 +5030,10 @@ Error: %@</source>
|
||||
<target>Test serveru se nezdařil!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server version is incompatible with network settings." xml:space="preserve">
|
||||
<source>Server version is incompatible with network settings.</source>
|
||||
<note>srv error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Servers" xml:space="preserve">
|
||||
<source>Servers</source>
|
||||
<target>Servery</target>
|
||||
@@ -5051,11 +5149,19 @@ Error: %@</source>
|
||||
<target>Zobrazit poslední zprávy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show message status" xml:space="preserve">
|
||||
<source>Show message status</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show preview" xml:space="preserve">
|
||||
<source>Show preview</source>
|
||||
<target>Zobrazení náhledu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show → on messages sent via private routing." xml:space="preserve">
|
||||
<source>Show → on messages sent via private routing.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show:" xml:space="preserve">
|
||||
<source>Show:</source>
|
||||
<target>Zobrazit:</target>
|
||||
@@ -5369,6 +5475,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
||||
<target>Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve">
|
||||
<source>The app will ask to confirm downloads from unknown file servers (except .onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve">
|
||||
<source>The attempt to change database passphrase was not completed.</source>
|
||||
<target>Pokus o změnu přístupové fráze databáze nebyl dokončen.</target>
|
||||
@@ -5545,6 +5655,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
||||
<target>K ochraně časového pásma používají obrazové/hlasové soubory UTC.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve">
|
||||
<source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve">
|
||||
<source>To protect your information, turn on SimpleX Lock.
|
||||
You will be prompted to complete authentication before this feature is enabled.</source>
|
||||
@@ -5631,11 +5745,6 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
|
||||
<source>Unblock member?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unexpected error: %@" xml:space="preserve">
|
||||
<source>Unexpected error: %@</source>
|
||||
<target>Neočekávaná chyba: %@</target>
|
||||
<note>item status description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unexpected migration state" xml:space="preserve">
|
||||
<source>Unexpected migration state</source>
|
||||
<target>Neočekávaný stav přenášení</target>
|
||||
@@ -5681,6 +5790,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
|
||||
<target>Neznámá chyba</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown servers!" xml:space="preserve">
|
||||
<source>Unknown servers!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
|
||||
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
|
||||
<target>Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování.</target>
|
||||
@@ -5821,6 +5934,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<source>Use only local notifications?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve">
|
||||
<source>Use private routing with unknown servers when IP address is not protected.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use private routing with unknown servers." xml:space="preserve">
|
||||
<source>Use private routing with unknown servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use server" xml:space="preserve">
|
||||
<source>Use server</source>
|
||||
<target>Použít server</target>
|
||||
@@ -6038,11 +6159,23 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<source>With reduced battery usage.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve">
|
||||
<source>Without Tor or VPN, your IP address will be visible to file servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
|
||||
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||
<source>Wrong database passphrase</source>
|
||||
<target>Špatná přístupová fráze k databázi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve">
|
||||
<source>Wrong key or unknown connection - most likely this connection is deleted.</source>
|
||||
<note>snd error text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong passphrase!" xml:space="preserve">
|
||||
<source>Wrong passphrase!</source>
|
||||
<target>Špatná přístupová fráze!</target>
|
||||
@@ -7122,6 +7255,12 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>odeslat přímou zprávu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve">
|
||||
<source>server queue info: %1$@
|
||||
|
||||
last received msg: %2$@</source>
|
||||
<note>queue info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="set new contact address" xml:space="preserve">
|
||||
<source>set new contact address</source>
|
||||
<note>profile update event chat item</note>
|
||||
@@ -7158,10 +7297,18 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>neznámý</target>
|
||||
<note>connection info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unknown relays" xml:space="preserve">
|
||||
<source>unknown relays</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unknown status" xml:space="preserve">
|
||||
<source>unknown status</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unprotected" xml:space="preserve">
|
||||
<source>unprotected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="updated group profile" xml:space="preserve">
|
||||
<source>updated group profile</source>
|
||||
<target>aktualizoval profil skupiny</target>
|
||||
@@ -7225,6 +7372,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>týdnů</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="when IP hidden" xml:space="preserve">
|
||||
<source>when IP hidden</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="yes" xml:space="preserve">
|
||||
<source>yes</source>
|
||||
<target>ano</target>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user