mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab5e7d02f | |||
| 29cc2cfb0c | |||
| 4826da0825 | |||
| 4f9f2cecec | |||
| a5c1be3b8f | |||
| f7cac769df | |||
| a4b086ce7a | |||
| 2a20afeda6 | |||
| beefb1ba12 | |||
| 6c1a821304 | |||
| ec40c2d7e5 | |||
| d6666b270f | |||
| 950b87b48f | |||
| ebbafc12d4 | |||
| 8053d8fc39 | |||
| d085a047fc | |||
| 99d5a183c4 | |||
| 8979306e68 | |||
| e5a60b963e |
@@ -123,6 +123,14 @@ class NetworkModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ChatItemWithMenu can depend on previous or next item for it's appearance
|
||||||
|
/// This dummy model is used to force an update of all chat items,
|
||||||
|
/// when they might have changed appearance.
|
||||||
|
class ChatItemDummyModel: ObservableObject {
|
||||||
|
static let shared = ChatItemDummyModel()
|
||||||
|
func sendUpdate() { objectWillChange.send() }
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatModel: ObservableObject {
|
final class ChatModel: ObservableObject {
|
||||||
@Published var onboardingStage: OnboardingStage?
|
@Published var onboardingStage: OnboardingStage?
|
||||||
@Published var setDeliveryReceipts = false
|
@Published var setDeliveryReceipts = false
|
||||||
@@ -428,19 +436,17 @@ final class ChatModel: ObservableObject {
|
|||||||
|
|
||||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||||
if let i = getChatItemIndex(cItem) {
|
if let i = getChatItemIndex(cItem) {
|
||||||
withConditionalAnimation {
|
_updateChatItem(at: i, with: cItem)
|
||||||
_updateChatItem(at: i, with: cItem)
|
ChatItemDummyModel.shared.sendUpdate()
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
withConditionalAnimation(itemAnimation()) {
|
var ci = cItem
|
||||||
var ci = cItem
|
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
ci.meta.itemStatus = status
|
||||||
ci.meta.itemStatus = status
|
|
||||||
}
|
|
||||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
|
||||||
im.itemAdded = true
|
|
||||||
}
|
}
|
||||||
|
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||||
|
im.itemAdded = true
|
||||||
|
ChatItemDummyModel.shared.sendUpdate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
|||||||
struct CIGroupInvitationView: View {
|
struct CIGroupInvitationView: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.showTimestamp) var showTimestamp
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
var groupInvitation: CIGroupInvitation
|
var groupInvitation: CIGroupInvitation
|
||||||
@@ -45,7 +46,7 @@ struct CIGroupInvitationView: View {
|
|||||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
+ Text(" ")
|
+ Text(" ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ struct CIGroupInvitationView: View {
|
|||||||
(
|
(
|
||||||
groupInvitationText()
|
groupInvitationText()
|
||||||
+ Text(" ")
|
+ Text(" ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
}
|
}
|
||||||
@@ -69,6 +70,7 @@ struct CIGroupInvitationView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||||
.background(chatItemFrameColor(chatItem, theme))
|
.background(chatItemFrameColor(chatItem, theme))
|
||||||
.textSelection(.disabled)
|
.textSelection(.disabled)
|
||||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
|||||||
struct CIMetaView: View {
|
struct CIMetaView: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.showTimestamp) var showTimestamp
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
var metaColor: Color
|
var metaColor: Color
|
||||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||||
@@ -30,24 +31,24 @@ struct CIMetaView: View {
|
|||||||
switch meta.itemStatus {
|
switch meta.itemStatus {
|
||||||
case let .sndSent(sndProgress):
|
case let .sndSent(sndProgress):
|
||||||
switch sndProgress {
|
switch sndProgress {
|
||||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
case let .sndRcvd(_, sndProgress):
|
case let .sndRcvd(_, sndProgress):
|
||||||
switch sndProgress {
|
switch sndProgress {
|
||||||
case .complete:
|
case .complete:
|
||||||
ZStack {
|
ZStack {
|
||||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
case .partial:
|
case .partial:
|
||||||
ZStack {
|
ZStack {
|
||||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,8 @@ func ciMetaText(
|
|||||||
sent: SentCheckmark? = nil,
|
sent: SentCheckmark? = nil,
|
||||||
showStatus: Bool = true,
|
showStatus: Bool = true,
|
||||||
showEdited: Bool = true,
|
showEdited: Bool = true,
|
||||||
showViaProxy: Bool
|
showViaProxy: Bool,
|
||||||
|
showTimesamp: Bool
|
||||||
) -> Text {
|
) -> Text {
|
||||||
var r = Text("")
|
var r = Text("")
|
||||||
if showEdited, meta.itemEdited {
|
if showEdited, meta.itemEdited {
|
||||||
@@ -105,7 +107,9 @@ func ciMetaText(
|
|||||||
if let enc = encrypted {
|
if let enc = encrypted {
|
||||||
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
|
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
|
||||||
}
|
}
|
||||||
r = r + meta.timestampText.foregroundColor(color)
|
if showTimesamp {
|
||||||
|
r = r + meta.timestampText.foregroundColor(color)
|
||||||
|
}
|
||||||
return r.font(.caption)
|
return r.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct CIRcvDecryptionError: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
|
@Environment(\.showTimestamp) var showTimestamp
|
||||||
var msgDecryptError: MsgDecryptError
|
var msgDecryptError: MsgDecryptError
|
||||||
var msgCount: UInt32
|
var msgCount: UInt32
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
@@ -68,37 +69,41 @@ struct CIRcvDecryptionError: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func viewBody() -> some View {
|
@ViewBuilder private func viewBody() -> some View {
|
||||||
if case let .direct(contact) = chat.chatInfo,
|
Group {
|
||||||
let contactStats = contact.activeConn?.connectionStats {
|
if case let .direct(contact) = chat.chatInfo,
|
||||||
if contactStats.ratchetSyncAllowed {
|
let contactStats = contact.activeConn?.connectionStats {
|
||||||
decryptionErrorItemFixButton(syncSupported: true) {
|
if contactStats.ratchetSyncAllowed {
|
||||||
alert = .syncAllowedAlert { syncContactConnection(contact) }
|
decryptionErrorItemFixButton(syncSupported: true) {
|
||||||
|
alert = .syncAllowedAlert { syncContactConnection(contact) }
|
||||||
|
}
|
||||||
|
} else if !contactStats.ratchetSyncSupported {
|
||||||
|
decryptionErrorItemFixButton(syncSupported: false) {
|
||||||
|
alert = .syncNotSupportedContactAlert
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
basicDecryptionErrorItem()
|
||||||
}
|
}
|
||||||
} else if !contactStats.ratchetSyncSupported {
|
} else if case let .group(groupInfo) = chat.chatInfo,
|
||||||
decryptionErrorItemFixButton(syncSupported: false) {
|
case let .groupRcv(groupMember) = chatItem.chatDir,
|
||||||
alert = .syncNotSupportedContactAlert
|
let mem = m.getGroupMember(groupMember.groupMemberId),
|
||||||
|
let memberStats = mem.wrapped.activeConn?.connectionStats {
|
||||||
|
if memberStats.ratchetSyncAllowed {
|
||||||
|
decryptionErrorItemFixButton(syncSupported: true) {
|
||||||
|
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
|
||||||
|
}
|
||||||
|
} else if !memberStats.ratchetSyncSupported {
|
||||||
|
decryptionErrorItemFixButton(syncSupported: false) {
|
||||||
|
alert = .syncNotSupportedMemberAlert
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
basicDecryptionErrorItem()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
basicDecryptionErrorItem()
|
basicDecryptionErrorItem()
|
||||||
}
|
}
|
||||||
} else if case let .group(groupInfo) = chat.chatInfo,
|
|
||||||
case let .groupRcv(groupMember) = chatItem.chatDir,
|
|
||||||
let mem = m.getGroupMember(groupMember.groupMemberId),
|
|
||||||
let memberStats = mem.wrapped.activeConn?.connectionStats {
|
|
||||||
if memberStats.ratchetSyncAllowed {
|
|
||||||
decryptionErrorItemFixButton(syncSupported: true) {
|
|
||||||
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
|
|
||||||
}
|
|
||||||
} else if !memberStats.ratchetSyncSupported {
|
|
||||||
decryptionErrorItemFixButton(syncSupported: false) {
|
|
||||||
alert = .syncNotSupportedMemberAlert
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
basicDecryptionErrorItem()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
basicDecryptionErrorItem()
|
|
||||||
}
|
}
|
||||||
|
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||||
|
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func basicDecryptionErrorItem() -> some View {
|
private func basicDecryptionErrorItem() -> some View {
|
||||||
@@ -122,7 +127,7 @@ struct CIRcvDecryptionError: View {
|
|||||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
+ Text(" ")
|
+ Text(" ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
@@ -131,7 +136,6 @@ struct CIRcvDecryptionError: View {
|
|||||||
}
|
}
|
||||||
.onTapGesture(perform: { onClick() })
|
.onTapGesture(perform: { onClick() })
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
|
||||||
.textSelection(.disabled)
|
.textSelection(.disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ struct CIRcvDecryptionError: View {
|
|||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
.italic()
|
.italic()
|
||||||
+ Text(" ")
|
+ Text(" ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||||
@@ -150,7 +154,6 @@ struct CIRcvDecryptionError: View {
|
|||||||
}
|
}
|
||||||
.onTapGesture(perform: { onClick() })
|
.onTapGesture(perform: { onClick() })
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
|
||||||
.textSelection(.disabled)
|
.textSelection(.disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ struct FramedItemView: View {
|
|||||||
.accessibilityLabel("")
|
.accessibilityLabel("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ struct CIMsgError: View {
|
|||||||
}
|
}
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||||
.textSelection(.disabled)
|
.textSelection(.disabled)
|
||||||
.onTapGesture(perform: onTap)
|
.onTapGesture(perform: onTap)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct MarkedDeletedItemView: View {
|
|||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||||
.background(chatItemFrameColor(chatItem, theme))
|
.background(chatItemFrameColor(chatItem, theme))
|
||||||
.textSelection(.disabled)
|
.textSelection(.disabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
|
|||||||
|
|
||||||
struct MsgContentView: View {
|
struct MsgContentView: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
|
@Environment(\.showTimestamp) var showTimestamp
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
var text: String
|
var text: String
|
||||||
var formattedText: [FormattedText]? = nil
|
var formattedText: [FormattedText]? = nil
|
||||||
@@ -84,7 +85,7 @@ struct MsgContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,21 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
struct ShowTimestamp: EnvironmentKey {
|
||||||
|
static let defaultValue: Bool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var showTimestamp: Bool {
|
||||||
|
get { self[ShowTimestamp.self] }
|
||||||
|
set { self[ShowTimestamp.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatItemView: View {
|
struct ChatItemView: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.showTimestamp) var showTimestamp
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
var maxWidth: CGFloat = .infinity
|
var maxWidth: CGFloat = .infinity
|
||||||
@Binding var revealed: Bool
|
@Binding var revealed: Bool
|
||||||
|
|||||||
@@ -696,6 +696,7 @@ struct ChatView: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@Binding @ObservedObject var chat: Chat
|
@Binding @ObservedObject var chat: Chat
|
||||||
|
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
|
||||||
let chatItem: ChatItem
|
let chatItem: ChatItem
|
||||||
let maxWidth: CGFloat
|
let maxWidth: CGFloat
|
||||||
@Binding var composeState: ComposeState
|
@Binding var composeState: ComposeState
|
||||||
@@ -716,31 +717,53 @@ struct ChatView: View {
|
|||||||
|
|
||||||
var revealed: Bool { chatItem == revealedChatItem }
|
var revealed: Bool { chatItem == revealedChatItem }
|
||||||
|
|
||||||
|
struct TimeSeparation {
|
||||||
|
let isTimestampShown: Bool
|
||||||
|
let isGapLarge: Bool
|
||||||
|
|
||||||
|
init(for chatItem: ChatItem, at index: Int?) {
|
||||||
|
let im = ItemsModel.shared
|
||||||
|
if let index, index > 0 && !im.reversedChatItems.isEmpty {
|
||||||
|
let nextItem = im.reversedChatItems[index - 1]
|
||||||
|
isGapLarge = nextItem.chatDir != chatItem.chatDir ||
|
||||||
|
nextItem.meta.createdAt.timeIntervalSince(chatItem.meta.createdAt) > 30
|
||||||
|
isTimestampShown = isGapLarge ||
|
||||||
|
formatTimestampText(chatItem.meta.createdAt) != formatTimestampText(nextItem.meta.createdAt)
|
||||||
|
} else {
|
||||||
|
isTimestampShown = true
|
||||||
|
isGapLarge = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let (currIndex, _) = m.getNextChatItem(chatItem)
|
let (currIndex, _) = m.getNextChatItem(chatItem)
|
||||||
let ciCategory = chatItem.mergeCategory
|
let ciCategory = chatItem.mergeCategory
|
||||||
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||||
let range = itemsRange(currIndex, prevHidden)
|
let range = itemsRange(currIndex, prevHidden)
|
||||||
|
let timeSeparation = TimeSeparation(for: chatItem, at: currIndex)
|
||||||
let im = ItemsModel.shared
|
let im = ItemsModel.shared
|
||||||
Group {
|
Group {
|
||||||
if revealed, let range = range {
|
if revealed, let range = range {
|
||||||
let items = Array(zip(Array(range), im.reversedChatItems[range]))
|
let items = Array(zip(Array(range), im.reversedChatItems[range]))
|
||||||
ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in
|
VStack(spacing: 0) {
|
||||||
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
|
||||||
chatItemView(ci, nil, prev)
|
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
||||||
.overlay {
|
chatItemView(ci, nil, prev, TimeSeparation(for: ci, at: i))
|
||||||
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
|
.overlay {
|
||||||
Color.clear
|
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
|
||||||
.contentShape(Rectangle())
|
Color.clear
|
||||||
.onTapGesture {
|
.contentShape(Rectangle())
|
||||||
let checked = selected.contains(ci.id)
|
.onTapGesture {
|
||||||
selectUnselectChatItem(select: !checked, ci)
|
let checked = selected.contains(ci.id)
|
||||||
|
selectUnselectChatItem(select: !checked, ci)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chatItemView(chatItem, range, prevItem)
|
chatItemView(chatItem, range, prevItem, timeSeparation)
|
||||||
.overlay {
|
.overlay {
|
||||||
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
||||||
Color.clear
|
Color.clear
|
||||||
@@ -791,7 +814,8 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View {
|
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ timeSeparation: TimeSeparation) -> some View {
|
||||||
|
let bottomPadding: Double = timeSeparation.isGapLarge ? 6 : 2
|
||||||
if case let .groupRcv(member) = ci.chatDir,
|
if case let .groupRcv(member) = ci.chatDir,
|
||||||
case let .group(groupInfo) = chat.chatInfo {
|
case let .group(groupInfo) = chat.chatInfo {
|
||||||
let (prevMember, memCount): (GroupMember?, Int) =
|
let (prevMember, memCount): (GroupMember?, Int) =
|
||||||
@@ -833,11 +857,11 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatItemWithMenu(ci, range, maxWidth)
|
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, bottomPadding)
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
} else {
|
} else {
|
||||||
@@ -846,11 +870,11 @@ struct ChatView: View {
|
|||||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
}
|
}
|
||||||
chatItemWithMenu(ci, range, maxWidth)
|
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
.padding(.leading, memberImageSize + 8 + 12)
|
.padding(.leading, memberImageSize + 8 + 12)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, bottomPadding)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
@@ -863,10 +887,10 @@ struct ChatView: View {
|
|||||||
.padding(.leading)
|
.padding(.leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatItemWithMenu(ci, range, maxWidth)
|
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, bottomPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,7 +905,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View {
|
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ timeSeparation: TimeSeparation) -> some View {
|
||||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||||
ChatItemView(
|
ChatItemView(
|
||||||
@@ -891,7 +915,8 @@ struct ChatView: View {
|
|||||||
revealed: .constant(revealed),
|
revealed: .constant(revealed),
|
||||||
allowMenu: $allowMenu
|
allowMenu: $allowMenu
|
||||||
)
|
)
|
||||||
.modifier(ChatItemClipped(ci))
|
.environment(\.showTimestamp, timeSeparation.isTimestampShown)
|
||||||
|
.modifier(ChatItemClipped(ci, isTailVisible: timeSeparation.isGapLarge))
|
||||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||||
.accessibilityLabel("")
|
.accessibilityLabel("")
|
||||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||||
|
|||||||
@@ -14,55 +14,150 @@ import SimpleXChat
|
|||||||
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
|
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
|
||||||
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
|
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
|
||||||
struct ChatItemClipped: ViewModifier {
|
struct ChatItemClipped: ViewModifier {
|
||||||
struct ClipShape: Shape {
|
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
|
||||||
let maxCornerRadius: Double
|
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var isTailEnabled = true
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
let chatItem: ChatItem?
|
||||||
Path(
|
let isTailVisible: Bool
|
||||||
roundedRect: rect,
|
|
||||||
cornerRadius: min((rect.height / 2), maxCornerRadius),
|
init() {
|
||||||
style: .circular
|
chatItem = nil
|
||||||
)
|
isTailVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ chatItem: ChatItem, isTailVisible: Bool) {
|
||||||
|
self.chatItem = chatItem
|
||||||
|
self.isTailVisible = isTailVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func itemShape() -> ChatItemShape {
|
||||||
|
if let chatItem {
|
||||||
|
switch chatItem.content {
|
||||||
|
case
|
||||||
|
.sndMsgContent,
|
||||||
|
.rcvMsgContent,
|
||||||
|
.rcvDecryptionError,
|
||||||
|
.rcvGroupInvitation,
|
||||||
|
.sndGroupInvitation,
|
||||||
|
.sndDeleted,
|
||||||
|
.rcvDeleted,
|
||||||
|
.rcvIntegrityError,
|
||||||
|
.sndModerated,
|
||||||
|
.rcvModerated,
|
||||||
|
.rcvBlocked,
|
||||||
|
.invalidJSON: isTailEnabled
|
||||||
|
? .bubble(
|
||||||
|
padding: chatItem.chatDir.sent ? .trailing : .leading,
|
||||||
|
isTailVisible: Self.hidesTail(chatItem.content.msgContent)
|
||||||
|
? false
|
||||||
|
: isTailVisible
|
||||||
|
)
|
||||||
|
: .roundRect(maxRadius: ChatBubble.maxRadius)
|
||||||
|
default: .roundRect(maxRadius: 8)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
.roundRect(maxRadius: 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
// Tail is hidden for images and video without any text
|
||||||
clipShape = ClipShape(
|
private static func hidesTail(_ msgContent: MsgContent?) -> Bool {
|
||||||
maxCornerRadius: 18
|
if let msgContent, msgContent.isImageOrVideo && msgContent.text.isEmpty {
|
||||||
)
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ chatItem: ChatItem) {
|
|
||||||
clipShape = ClipShape(
|
|
||||||
maxCornerRadius: {
|
|
||||||
switch chatItem.content {
|
|
||||||
case
|
|
||||||
.sndMsgContent,
|
|
||||||
.rcvMsgContent,
|
|
||||||
.rcvDecryptionError,
|
|
||||||
.rcvGroupInvitation,
|
|
||||||
.sndGroupInvitation,
|
|
||||||
.sndDeleted,
|
|
||||||
.rcvDeleted,
|
|
||||||
.rcvIntegrityError,
|
|
||||||
.sndModerated,
|
|
||||||
.rcvModerated,
|
|
||||||
.rcvBlocked,
|
|
||||||
.invalidJSON: 18
|
|
||||||
default: 8
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private let clipShape: ClipShape
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
let shape = ChatBubble(roundness: roundness, shapePath: itemShape())
|
||||||
content
|
content
|
||||||
.contentShape(.dragPreview, clipShape)
|
.contentShape(.dragPreview, shape)
|
||||||
.contentShape(.contextMenuPreview, clipShape)
|
.contentShape(.contextMenuPreview, shape)
|
||||||
.clipShape(clipShape)
|
.clipShape(shape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ChatTailPadding: ViewModifier {
|
||||||
|
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
|
||||||
|
let chatItem: ChatItem
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if tailEnabled {
|
||||||
|
content.padding(
|
||||||
|
chatItem.chatDir.sent ? .trailing : .leading,
|
||||||
|
ChatBubble.tailSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatBubble: Shape {
|
||||||
|
static let tailSize: Double = 8
|
||||||
|
static let maxRadius: Double = 16
|
||||||
|
fileprivate let roundness: Double
|
||||||
|
fileprivate let shapePath: ChatItemShape
|
||||||
|
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
switch shapePath {
|
||||||
|
case .bubble(let padding, let isTailVisible):
|
||||||
|
let rMax = min(Self.maxRadius, min(rect.width, rect.height) / 2)
|
||||||
|
let r = roundness * rMax
|
||||||
|
let tailHeight = rect.height - (Self.tailSize + (rMax - Self.tailSize) * roundness)
|
||||||
|
var path = Path()
|
||||||
|
path.addArc(
|
||||||
|
center: CGPoint(x: r + Self.tailSize, y: r),
|
||||||
|
radius: r,
|
||||||
|
startAngle: .degrees(270),
|
||||||
|
endAngle: .degrees(180),
|
||||||
|
clockwise: true
|
||||||
|
)
|
||||||
|
if isTailVisible {
|
||||||
|
path.addLine(
|
||||||
|
to: CGPoint(x: Self.tailSize, y: tailHeight)
|
||||||
|
)
|
||||||
|
path.addQuadCurve(
|
||||||
|
to: CGPoint(x: 0, y: rect.height),
|
||||||
|
control: CGPoint(x: Self.tailSize, y: tailHeight + r * 0.64)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
path.addArc(
|
||||||
|
center: CGPoint(x: r + Self.tailSize, y: rect.height - r),
|
||||||
|
radius: r,
|
||||||
|
startAngle: .degrees(180),
|
||||||
|
endAngle: .degrees(90),
|
||||||
|
clockwise: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
path.addArc(
|
||||||
|
center: CGPoint(x: rect.width - r, y: rect.height - r),
|
||||||
|
radius: r,
|
||||||
|
startAngle: .degrees(90),
|
||||||
|
endAngle: .degrees(0),
|
||||||
|
clockwise: true
|
||||||
|
)
|
||||||
|
path.addArc(
|
||||||
|
center: CGPoint(x: rect.width - r, y: r),
|
||||||
|
radius: r,
|
||||||
|
startAngle: .degrees(0),
|
||||||
|
endAngle: .degrees(270),
|
||||||
|
clockwise: true
|
||||||
|
)
|
||||||
|
return switch padding {
|
||||||
|
case .leading: path
|
||||||
|
case .trailing: path
|
||||||
|
.scale(x: -1, y: 1, anchor: .center)
|
||||||
|
.path(in: rect)
|
||||||
|
}
|
||||||
|
case let .roundRect(radius):
|
||||||
|
return Path(roundedRect: rect, cornerRadius: radius * roundness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate enum ChatItemShape {
|
||||||
|
case bubble(padding: HorizontalEdge, isTailVisible: Bool)
|
||||||
|
case roundRect(maxRadius: Double)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ struct AppearanceSettings: View {
|
|||||||
}()
|
}()
|
||||||
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
|
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
|
||||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||||
|
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness
|
||||||
|
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true
|
||||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
|
||||||
@@ -179,6 +181,14 @@ struct AppearanceSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Messages").foregroundColor(theme.colors.secondary)) {
|
||||||
|
HStack {
|
||||||
|
Text("Roundness")
|
||||||
|
Slider(value: $chatItemRoundness, in: 0...1, step: 0.1)
|
||||||
|
}
|
||||||
|
Toggle("Tail", isOn: $chatItemTail)
|
||||||
|
}
|
||||||
|
|
||||||
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
if let img = m.currentUser?.image, img != "" {
|
if let img = m.currentUser?.image, img != "" {
|
||||||
@@ -358,13 +368,13 @@ struct ChatThemePreview: View {
|
|||||||
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
||||||
HStack {
|
HStack {
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
|
||||||
.modifier(ChatItemClipped())
|
.modifier(ChatItemClipped(alice, isTailVisible: true))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
|
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
|
||||||
.modifier(ChatItemClipped())
|
.modifier(ChatItemClipped(bob, isTailVisible: true))
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for
|
|||||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
|
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
|
||||||
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration
|
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration
|
||||||
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||||
|
let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
|
||||||
|
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
|
||||||
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
|
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
|
||||||
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
|
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
|
||||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||||
@@ -98,6 +100,8 @@ let appDefaults: [String: Any] = [
|
|||||||
DEFAULT_DEVELOPER_TOOLS: false,
|
DEFAULT_DEVELOPER_TOOLS: false,
|
||||||
DEFAULT_ENCRYPTION_STARTED: false,
|
DEFAULT_ENCRYPTION_STARTED: false,
|
||||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||||
|
DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
|
||||||
|
DEFAULT_CHAT_ITEM_TAIL: true,
|
||||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
|
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
|
||||||
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
||||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public let appSuspendTimeout: Int = 15 // seconds
|
|||||||
|
|
||||||
public let defaultProfileImageCorner: Double = 22.5
|
public let defaultProfileImageCorner: Double = 22.5
|
||||||
|
|
||||||
|
public let defaultChatItemRoundness: Double = 0.8
|
||||||
|
|
||||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||||
let GROUP_DEFAULT_SE_STATE = "seState"
|
let GROUP_DEFAULT_SE_STATE = "seState"
|
||||||
|
|||||||
@@ -2762,7 +2762,7 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
|
|||||||
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
||||||
|
|
||||||
public func formatTimestampText(_ date: Date) -> Text {
|
public func formatTimestampText(_ date: Date) -> Text {
|
||||||
return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat)
|
Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func recent(_ date: Date) -> Bool {
|
private func recent(_ date: Date) -> Bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user