mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-10 21:37:09 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -43,6 +43,21 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
class ItemsModel: ObservableObject {
|
||||
static let shared = ItemsModel()
|
||||
private let publisher = ObservableObjectPublisher()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
var reversedChatItems: [ChatItem] = [] {
|
||||
willSet { publisher.send() }
|
||||
}
|
||||
init() {
|
||||
publisher
|
||||
.throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { self.objectWillChange.send() }
|
||||
.store(in: &bag)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var setDeliveryReceipts = false
|
||||
@@ -69,7 +84,6 @@ final class ChatModel: ObservableObject {
|
||||
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var reversedChatItems: [ChatItem] = []
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GMember] = []
|
||||
@@ -117,6 +131,8 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
let im = ItemsModel.shared
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
let ntfEnableLocal = true
|
||||
@@ -343,7 +359,7 @@ final class ChatModel: ObservableObject {
|
||||
var res: Bool
|
||||
if let chat = getChat(cInfo.id) {
|
||||
if let pItem = chat.chatItems.last {
|
||||
if pItem.id == cItem.id || (chatId == cInfo.id && reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
|
||||
if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
} else {
|
||||
@@ -373,7 +389,7 @@ final class ChatModel: ObservableObject {
|
||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||
ci.meta.itemStatus = status
|
||||
}
|
||||
reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -397,12 +413,12 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
|
||||
reversedChatItems[i] = cItem
|
||||
reversedChatItems[i].viewTimestamp = .now
|
||||
im.reversedChatItems[i] = cItem
|
||||
im.reversedChatItems[i].viewTimestamp = .now
|
||||
}
|
||||
|
||||
func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
im.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
}
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
@@ -419,7 +435,7 @@ final class ChatModel: ObservableObject {
|
||||
if chatId == cInfo.id {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
_ = withAnimation {
|
||||
self.reversedChatItems.remove(at: i)
|
||||
im.reversedChatItems.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,16 +443,16 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func nextChatItemData<T>(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? {
|
||||
guard var i = reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil }
|
||||
guard var i = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil }
|
||||
if previous {
|
||||
while i < reversedChatItems.count - 1 {
|
||||
while i < im.reversedChatItems.count - 1 {
|
||||
i += 1
|
||||
if let res = map(reversedChatItems[i]) { return res }
|
||||
if let res = map(im.reversedChatItems[i]) { return res }
|
||||
}
|
||||
} else {
|
||||
while i > 0 {
|
||||
i -= 1
|
||||
if let res = map(reversedChatItems[i]) { return res }
|
||||
if let res = map(im.reversedChatItems[i]) { return res }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -467,7 +483,7 @@ final class ChatModel: ObservableObject {
|
||||
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
reversedChatItems.insert(cItem, at: 0)
|
||||
im.reversedChatItems.insert(cItem, at: 0)
|
||||
}
|
||||
return cItem
|
||||
}
|
||||
@@ -475,15 +491,15 @@ final class ChatModel: ObservableObject {
|
||||
func removeLiveDummy(animated: Bool = true) {
|
||||
if hasLiveDummy {
|
||||
if animated {
|
||||
withAnimation { _ = reversedChatItems.removeFirst() }
|
||||
withAnimation { _ = im.reversedChatItems.removeFirst() }
|
||||
} else {
|
||||
_ = reversedChatItems.removeFirst()
|
||||
_ = im.reversedChatItems.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasLiveDummy: Bool {
|
||||
reversedChatItems.first?.isLiveDummy == true
|
||||
im.reversedChatItems.first?.isLiveDummy == true
|
||||
}
|
||||
|
||||
func markChatItemsRead(_ cInfo: ChatInfo) {
|
||||
@@ -500,7 +516,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
private func markCurrentChatRead(fromIndex i: Int = 0) {
|
||||
var j = i
|
||||
while j < reversedChatItems.count {
|
||||
while j < im.reversedChatItems.count {
|
||||
markChatItemRead_(j)
|
||||
j += 1
|
||||
}
|
||||
@@ -514,7 +530,7 @@ final class ChatModel: ObservableObject {
|
||||
var unreadBelow = 0
|
||||
var j = i - 1
|
||||
while j >= 0 {
|
||||
if case .rcvNew = self.reversedChatItems[j].meta.itemStatus {
|
||||
if case .rcvNew = self.im.reversedChatItems[j].meta.itemStatus {
|
||||
unreadBelow += 1
|
||||
}
|
||||
j -= 1
|
||||
@@ -549,7 +565,7 @@ final class ChatModel: ObservableObject {
|
||||
// clear current chat
|
||||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
reversedChatItems = []
|
||||
im.reversedChatItems = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,32 +573,58 @@ final class ChatModel: ObservableObject {
|
||||
if chatId == cInfo.id,
|
||||
let itemIndex = getChatItemIndex(cItem),
|
||||
let chatIndex = getChatIndex(cInfo.id),
|
||||
reversedChatItems[itemIndex].isRcvNew {
|
||||
im.reversedChatItems[itemIndex].isRcvNew {
|
||||
await MainActor.run {
|
||||
withTransaction(Transaction()) {
|
||||
// update current chat
|
||||
markChatItemRead_(itemIndex)
|
||||
// update preview
|
||||
decreaseUnreadCounter(chatIndex)
|
||||
unreadCollector.decreaseUnreadCounter(chatIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let unreadCollector = UnreadCollector()
|
||||
|
||||
class UnreadCollector {
|
||||
private let subject = PassthroughSubject<Int, Never>()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
private var dictionary = Dictionary<Int, Int>()
|
||||
|
||||
init() {
|
||||
subject
|
||||
.debounce(for: 1, scheduler: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
self.dictionary.forEach { key, value in
|
||||
ChatModel.shared.decreaseUnreadCounter(key, by: value)
|
||||
}
|
||||
self.dictionary = Dictionary<Int, Int>()
|
||||
}
|
||||
.store(in: &bag)
|
||||
}
|
||||
|
||||
// Only call from main thread
|
||||
func decreaseUnreadCounter(_ chatIndex: Int) {
|
||||
dictionary[chatIndex] = (dictionary[chatIndex] ?? 0) + 1
|
||||
subject.send(chatIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private func markChatItemRead_(_ i: Int) {
|
||||
let meta = reversedChatItems[i].meta
|
||||
let meta = im.reversedChatItems[i].meta
|
||||
if case .rcvNew = meta.itemStatus {
|
||||
reversedChatItems[i].meta.itemStatus = .rcvRead
|
||||
reversedChatItems[i].viewTimestamp = .now
|
||||
im.reversedChatItems[i].meta.itemStatus = .rcvRead
|
||||
im.reversedChatItems[i].viewTimestamp = .now
|
||||
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
|
||||
reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
||||
im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(_ chatIndex: Int) {
|
||||
chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount - 1
|
||||
decreaseUnreadCounter(user: currentUser!)
|
||||
func decreaseUnreadCounter(_ chatIndex: Int, by count: Int = 1) {
|
||||
chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount - count
|
||||
decreaseUnreadCounter(user: currentUser!, by: count)
|
||||
}
|
||||
|
||||
func increaseUnreadCounter(user: any UserLike) {
|
||||
@@ -612,8 +654,8 @@ final class ChatModel: ObservableObject {
|
||||
var ns: [String] = []
|
||||
if let ciCategory = chatItem.mergeCategory,
|
||||
var i = getChatItemIndex(chatItem) {
|
||||
while i < reversedChatItems.count {
|
||||
let ci = reversedChatItems[i]
|
||||
while i < im.reversedChatItems.count {
|
||||
let ci = im.reversedChatItems[i]
|
||||
if ci.mergeCategory != ciCategory { break }
|
||||
if let m = ci.memberConnected {
|
||||
ns.append(m.displayName)
|
||||
@@ -628,7 +670,7 @@ final class ChatModel: ObservableObject {
|
||||
// returns the index of the passed item and the next item (it has smaller index)
|
||||
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) {
|
||||
if let i = getChatItemIndex(ci) {
|
||||
(i, i > 0 ? reversedChatItems[i - 1] : nil)
|
||||
(i, i > 0 ? im.reversedChatItems[i - 1] : nil)
|
||||
} else {
|
||||
(nil, nil)
|
||||
}
|
||||
@@ -638,10 +680,10 @@ final class ChatModel: ObservableObject {
|
||||
// and the previous visible item with another merge category
|
||||
func getPrevShownChatItem(_ ciIndex: Int?, _ ciCategory: CIMergeCategory?) -> (Int?, ChatItem?) {
|
||||
guard var i = ciIndex else { return (nil, nil) }
|
||||
let fst = reversedChatItems.count - 1
|
||||
let fst = im.reversedChatItems.count - 1
|
||||
while i < fst {
|
||||
i = i + 1
|
||||
let ci = reversedChatItems[i]
|
||||
let ci = im.reversedChatItems[i]
|
||||
if ciCategory == nil || ciCategory != ci.mergeCategory {
|
||||
return (i - 1, ci)
|
||||
}
|
||||
@@ -654,7 +696,7 @@ final class ChatModel: ObservableObject {
|
||||
var prevMember: GroupMember? = nil
|
||||
var memberIds: Set<Int64> = []
|
||||
for i in range {
|
||||
if case let .groupRcv(m) = reversedChatItems[i].chatDir {
|
||||
if case let .groupRcv(m) = im.reversedChatItems[i].chatDir {
|
||||
if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m }
|
||||
memberIds.insert(m.groupMemberId)
|
||||
}
|
||||
@@ -729,9 +771,9 @@ final class ChatModel: ObservableObject {
|
||||
var i = 0
|
||||
var totalBelow = 0
|
||||
var unreadBelow = 0
|
||||
while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) {
|
||||
while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
|
||||
totalBelow += 1
|
||||
if reversedChatItems[i].isRcvNew {
|
||||
if im.reversedChatItems[i].isRcvNew {
|
||||
unreadBelow += 1
|
||||
}
|
||||
i += 1
|
||||
@@ -740,12 +782,12 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
||||
let maxIx = reversedChatItems.count - 1
|
||||
let maxIx = im.reversedChatItems.count - 1
|
||||
var i = 0
|
||||
let inView = { itemsInView.contains(self.reversedChatItems[$0].viewId) }
|
||||
let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) }
|
||||
while i < maxIx && !inView(i) { i += 1 }
|
||||
while i < maxIx && inView(i) { i += 1 }
|
||||
return reversedChatItems[min(i - 1, maxIx)]
|
||||
return im.reversedChatItems[min(i - 1, maxIx)]
|
||||
}
|
||||
|
||||
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
||||
|
||||
@@ -225,6 +225,15 @@ func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCheckChatRunning() throws -> Bool {
|
||||
let r = chatSendCmdSync(.checkChatRunning)
|
||||
switch r {
|
||||
case .chatRunning: return true
|
||||
case .chatStopped: return false
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiStopChat() async throws {
|
||||
let r = await chatSendCmd(.apiStopChat)
|
||||
switch r {
|
||||
@@ -325,11 +334,12 @@ func loadChat(chat: Chat, search: String = "") {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let m = ChatModel.shared
|
||||
let im = ItemsModel.shared
|
||||
m.chatItemStatuses = [:]
|
||||
m.reversedChatItems = []
|
||||
im.reversedChatItems = []
|
||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
||||
m.updateChatInfo(chat.chatInfo)
|
||||
m.reversedChatItems = chat.chatItems.reversed()
|
||||
im.reversedChatItems = chat.chatItems.reversed()
|
||||
} catch let error {
|
||||
logger.error("loadChat error: \(responseError(error))")
|
||||
}
|
||||
@@ -1439,15 +1449,16 @@ func startChat(refreshInvitations: Bool = true) throws {
|
||||
logger.debug("startChat")
|
||||
let m = ChatModel.shared
|
||||
try setNetworkConfig(getNetCfg())
|
||||
let justStarted = try apiStartChat()
|
||||
let chatRunning = try apiCheckChatRunning()
|
||||
m.users = try listUsers()
|
||||
if justStarted {
|
||||
if !chatRunning {
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
|
||||
if (refreshInvitations) {
|
||||
try refreshCallInvitations()
|
||||
}
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
||||
_ = try apiStartChat()
|
||||
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
|
||||
// when it is called before startChat
|
||||
if let token = m.deviceToken {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var im = ItemsModel.shared
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
@@ -53,8 +54,8 @@ struct CIChatFeatureView: View {
|
||||
var fs: [FeatureInfo] = []
|
||||
var icons: Set<String> = []
|
||||
if var i = m.getChatItemIndex(chatItem) {
|
||||
while i < m.reversedChatItems.count,
|
||||
let f = featureInfo(m.reversedChatItems[i]) {
|
||||
while i < im.reversedChatItems.count,
|
||||
let f = featureInfo(im.reversedChatItems[i]) {
|
||||
if !icons.contains(f.icon) {
|
||||
fs.insert(f, at: 0)
|
||||
icons.insert(f.icon)
|
||||
|
||||
@@ -14,10 +14,10 @@ struct CIFileView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
var smallView: Bool = false
|
||||
var smallViewSize: CGFloat?
|
||||
|
||||
var body: some View {
|
||||
if smallView {
|
||||
if smallViewSize != nil {
|
||||
fileIndicator()
|
||||
.onTapGesture(perform: fileAction)
|
||||
} else {
|
||||
@@ -201,21 +201,22 @@ struct CIFileView: View {
|
||||
}
|
||||
|
||||
private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View {
|
||||
ZStack(alignment: .center) {
|
||||
let size = smallViewSize ?? 30
|
||||
return ZStack(alignment: .center) {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: smallView ? 36 : 30, height: smallView ? 36 : 30)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(color)
|
||||
if let innerIcon = innerIcon,
|
||||
let innerIconSize = innerIconSize, (!smallView || file?.showStatusIconInSmallView == true) {
|
||||
let innerIconSize = innerIconSize, (smallViewSize == nil || file?.showStatusIconInSmallView == true) {
|
||||
Image(systemName: innerIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 16)
|
||||
.frame(width: innerIconSize, height: innerIconSize)
|
||||
.foregroundColor(.white)
|
||||
.padding(.top, smallView ? 15 : 12)
|
||||
.padding(.top, size / 2.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ struct CIVoiceView: View {
|
||||
@State var playbackTime: TimeInterval? = nil
|
||||
|
||||
@Binding var allowMenu: Bool
|
||||
var smallView: Bool = false
|
||||
var smallViewSize: CGFloat?
|
||||
@State private var seek: (TimeInterval) -> Void = { _ in }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if smallView {
|
||||
if smallViewSize != nil {
|
||||
HStack(spacing: 10) {
|
||||
player()
|
||||
playerTime()
|
||||
@@ -65,7 +65,12 @@ struct CIVoiceView: View {
|
||||
}
|
||||
|
||||
private func player() -> some View {
|
||||
VoiceMessagePlayer(
|
||||
let sizeMultiplier: CGFloat = if let sz = smallViewSize {
|
||||
voiceMessageSizeBasedOnSquareSize(sz) / 56
|
||||
} else {
|
||||
1
|
||||
}
|
||||
return VoiceMessagePlayer(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
recordingFile: recordingFile,
|
||||
@@ -76,7 +81,7 @@ struct CIVoiceView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime,
|
||||
allowMenu: $allowMenu,
|
||||
sizeMultiplier: smallView ? voiceMessageSizeBasedOnSquareSize(36) / 56 : 1
|
||||
sizeMultiplier: sizeMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ struct FramedItemView: View {
|
||||
if let qi = chatItem.quotedItem {
|
||||
ciQuoteView(qi)
|
||||
.onTapGesture {
|
||||
if let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
withAnimation {
|
||||
scrollModel.scrollToItem(id: ci.id)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ struct MarkedDeletedItemView: View {
|
||||
var blockedByAdmin = 0
|
||||
var deleted = 0
|
||||
var moderatedBy: Set<String> = []
|
||||
while i < m.reversedChatItems.count,
|
||||
let ci = .some(m.reversedChatItems[i]),
|
||||
while i < ItemsModel.shared.reversedChatItems.count,
|
||||
let ci = .some(ItemsModel.shared.reversedChatItems[i]),
|
||||
ci.mergeCategory == ciCategory,
|
||||
let itemDeleted = ci.meta.itemDeleted {
|
||||
switch itemDeleted {
|
||||
|
||||
@@ -15,6 +15,7 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var im = ItemsModel.shared
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -110,7 +111,7 @@ struct ChatView: View {
|
||||
.onChange(of: revealedChatItem) { _ in
|
||||
NotificationCenter.postReverseListNeedsLayout()
|
||||
}
|
||||
.onChange(of: chatModel.reversedChatItems) { reversedChatItems in
|
||||
.onChange(of: im.reversedChatItems) { reversedChatItems in
|
||||
if reversedChatItems.count <= loadItemsPerPage && filtered(reversedChatItems).count < 10 {
|
||||
loadChatItems(chat.chatInfo)
|
||||
}
|
||||
@@ -124,7 +125,7 @@ struct ChatView: View {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
chatModel.reversedChatItems = []
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
chatModel.groupMembers = []
|
||||
chatModel.groupMembersIndexes.removeAll()
|
||||
chatModel.membersLoaded = false
|
||||
@@ -339,7 +340,7 @@ struct ChatView: View {
|
||||
|
||||
private func chatItemsList() -> some View {
|
||||
let cInfo = chat.chatInfo
|
||||
let mergedItems = filtered(chatModel.reversedChatItems)
|
||||
let mergedItems = filtered(im.reversedChatItems)
|
||||
return GeometryReader { g in
|
||||
ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in
|
||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||
@@ -372,7 +373,7 @@ struct ChatView: View {
|
||||
loadChat(chat: c)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.reversedChatItems) { _ in
|
||||
.onChange(of: im.reversedChatItems) { _ in
|
||||
floatingButtonModel.chatItemsChanged()
|
||||
}
|
||||
}
|
||||
@@ -562,7 +563,7 @@ struct ChatView: View {
|
||||
// Load additional items until the page is +50 large after merging
|
||||
while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage {
|
||||
let pagination: ChatPagination =
|
||||
if let lastItem = reversedPage.last ?? chatModel.reversedChatItems.last {
|
||||
if let lastItem = reversedPage.last ?? im.reversedChatItems.last {
|
||||
.before(chatItemId: lastItem.id, count: loadItemsPerPage)
|
||||
} else {
|
||||
.last(count: loadItemsPerPage)
|
||||
@@ -580,7 +581,7 @@ struct ChatView: View {
|
||||
if reversedPage.count == 0 {
|
||||
firstPage = true
|
||||
} else {
|
||||
chatModel.reversedChatItems.append(contentsOf: reversedPage)
|
||||
im.reversedChatItems.append(contentsOf: reversedPage)
|
||||
}
|
||||
loadingItems = false
|
||||
}
|
||||
@@ -634,11 +635,12 @@ struct ChatView: View {
|
||||
let ciCategory = chatItem.mergeCategory
|
||||
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||
let range = itemsRange(currIndex, prevHidden)
|
||||
let im = ItemsModel.shared
|
||||
Group {
|
||||
if revealed, let range = range {
|
||||
let items = Array(zip(Array(range), m.reversedChatItems[range]))
|
||||
let items = Array(zip(Array(range), im.reversedChatItems[range]))
|
||||
ForEach(items, id: \.1.viewId) { (i, ci) in
|
||||
let prev = i == prevHidden ? prevItem : m.reversedChatItems[i + 1]
|
||||
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
||||
chatItemView(ci, nil, prev)
|
||||
}
|
||||
} else {
|
||||
@@ -646,23 +648,39 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
markRead(
|
||||
chatItems: range.flatMap { m.reversedChatItems[$0] }
|
||||
?? [chatItem]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func markRead(chatItems: Array<ChatItem>.SubSequence) {
|
||||
let unreadItems = chatItems.filter { $0.isRcvNew }
|
||||
if unreadItems.isEmpty { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||||
if m.chatId == chat.chatInfo.id {
|
||||
Task {
|
||||
for unreadItem in unreadItems {
|
||||
await apiMarkChatItemRead(chat.chatInfo, unreadItem)
|
||||
if let range {
|
||||
if let items = unreadItems(range) {
|
||||
waitToMarkRead {
|
||||
for ci in items {
|
||||
await apiMarkChatItemRead(chat.chatInfo, ci)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if chatItem.isRcvNew {
|
||||
waitToMarkRead {
|
||||
await apiMarkChatItemRead(chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadItems(_ range: ClosedRange<Int>) -> [ChatItem]? {
|
||||
let im = ItemsModel.shared
|
||||
let items = range.compactMap { i in
|
||||
if i >= 0 && i < im.reversedChatItems.count {
|
||||
let ci = im.reversedChatItems[i]
|
||||
return if ci.isRcvNew { ci } else { nil }
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return if items.isEmpty { nil } else { items }
|
||||
}
|
||||
|
||||
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||||
if m.chatId == chat.chatInfo.id {
|
||||
Task(operation: op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1141,7 +1159,7 @@ struct ChatView: View {
|
||||
if let range = itemsRange(currIndex, prevHidden) {
|
||||
var itemIds: [Int64] = []
|
||||
for i in range {
|
||||
itemIds.append(m.reversedChatItems[i].id)
|
||||
itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
|
||||
}
|
||||
showDeleteMessages = true
|
||||
deletingItems = itemIds
|
||||
@@ -1384,7 +1402,7 @@ struct ChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chatId = "@1"
|
||||
chatModel.reversedChatItems = [
|
||||
ItemsModel.shared.reversedChatItems = [
|
||||
ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
ChatItem.getSample(2, .directRcv, .now, "hi"),
|
||||
ChatItem.getSample(3, .directRcv, .now, "hi there"),
|
||||
|
||||
@@ -257,6 +257,9 @@ struct SendMessageView: View {
|
||||
var body: some View {
|
||||
Button(action: {}) {
|
||||
Image(systemName: "mic.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(disabled)
|
||||
@@ -310,6 +313,9 @@ struct SendMessageView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "mic")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
|
||||
@@ -9,25 +9,41 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let rowHeights: [DynamicTypeSize: CGFloat] = [
|
||||
.xSmall: 68,
|
||||
.small: 72,
|
||||
.medium: 76,
|
||||
.large: 80,
|
||||
.xLarge: 88,
|
||||
.xxLarge: 94,
|
||||
.xxxLarge: 104,
|
||||
.accessibility1: 90,
|
||||
.accessibility2: 100,
|
||||
.accessibility3: 120,
|
||||
.accessibility4: 130,
|
||||
.accessibility5: 140
|
||||
typealias DynamicSizes = (
|
||||
rowHeight: CGFloat,
|
||||
profileImageSize: CGFloat,
|
||||
mediaSize: CGFloat,
|
||||
incognitoSize: CGFloat,
|
||||
chatInfoSize: CGFloat,
|
||||
unreadCorner: CGFloat,
|
||||
unreadPadding: CGFloat
|
||||
)
|
||||
|
||||
private let dynamicSizes: [DynamicTypeSize: DynamicSizes] = [
|
||||
.xSmall: (68, 55, 33, 22, 18, 9, 3),
|
||||
.small: (72, 57, 34, 22, 18, 9, 3),
|
||||
.medium: (76, 60, 36, 22, 18, 10, 4),
|
||||
.large: (80, 63, 38, 24, 20, 10, 4),
|
||||
.xLarge: (88, 67, 41, 24, 20, 10, 4),
|
||||
.xxLarge: (100, 71, 44, 27, 22, 11, 4),
|
||||
.xxxLarge: (110, 75, 48, 30, 24, 12, 5),
|
||||
.accessibility1: (110, 75, 48, 30, 24, 12, 5),
|
||||
.accessibility2: (114, 75, 48, 30, 24, 12, 5),
|
||||
.accessibility3: (124, 75, 48, 30, 24, 12, 5),
|
||||
.accessibility4: (134, 75, 48, 30, 24, 12, 5),
|
||||
.accessibility5: (144, 75, 48, 30, 24, 12, 5)
|
||||
]
|
||||
|
||||
private let defaultDynamicSizes: DynamicSizes = dynamicSizes[.large]!
|
||||
|
||||
func dynamicSize(_ font: DynamicTypeSize) -> DynamicSizes {
|
||||
dynamicSizes[font] ?? defaultDynamicSizes
|
||||
}
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showContactRequestDialog = false
|
||||
@State private var showJoinGroupDialog = false
|
||||
@@ -38,6 +54,8 @@ struct ChatListNavLink: View {
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
var dynamicRowHeight: CGFloat { dynamicSizes[userFont]?.rowHeight ?? 80 }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch chat.chatInfo {
|
||||
@@ -70,7 +88,7 @@ struct ChatListNavLink: View {
|
||||
Group {
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button {
|
||||
showDeleteContactActionSheet = true
|
||||
@@ -110,7 +128,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
}
|
||||
}
|
||||
.actionSheet(isPresented: $showDeleteContactActionSheet) {
|
||||
@@ -139,7 +157,7 @@ struct ChatListNavLink: View {
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton()
|
||||
if groupInfo.canDelete {
|
||||
@@ -159,7 +177,7 @@ struct ChatListNavLink: View {
|
||||
.disabled(inProgress)
|
||||
case .memAccepted:
|
||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
|
||||
}
|
||||
@@ -178,7 +196,7 @@ struct ChatListNavLink: View {
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !groupInfo.ready
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
@@ -205,7 +223,7 @@ struct ChatListNavLink: View {
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !noteFolder.ready
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
}
|
||||
@@ -321,7 +339,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showContactRequestDialog = true }
|
||||
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
|
||||
@@ -349,7 +367,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
Group {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
@@ -469,7 +487,7 @@ struct ChatListNavLink: View {
|
||||
Text("invalid chat data")
|
||||
.foregroundColor(.red)
|
||||
.padding(4)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var progressByTimeout: Bool
|
||||
@State var deleting: Bool = false
|
||||
@@ -21,11 +22,14 @@ struct ChatPreviewView: View {
|
||||
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
|
||||
var dynamicMediaSize: CGFloat { dynamicSize(userFont).mediaSize }
|
||||
var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize }
|
||||
|
||||
var body: some View {
|
||||
let cItem = chat.chatItems.last
|
||||
return HStack(spacing: 8) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ChatInfoImage(chat: chat, size: 63)
|
||||
ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize)
|
||||
chatPreviewImageOverlayIcon()
|
||||
.padding([.bottom, .trailing], 1)
|
||||
}
|
||||
@@ -73,7 +77,7 @@ struct ChatPreviewView: View {
|
||||
checkActiveContentPreview(chat, ci, mc)
|
||||
}
|
||||
chatStatusImage()
|
||||
.padding(.top, 26)
|
||||
.padding(.top, dynamicChatInfoSize * 1.44)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -172,7 +176,7 @@ struct ChatPreviewView: View {
|
||||
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
let t = text
|
||||
.lineLimit(2)
|
||||
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, hasFilePreview ? 0 : 8)
|
||||
@@ -192,22 +196,25 @@ struct ChatPreviewView: View {
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
.font(.caption)
|
||||
.font(userFont <= .xxxLarge ? .caption : .caption2)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
|
||||
.frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
|
||||
.cornerRadius(10)
|
||||
.cornerRadius(dynamicSize(userFont).unreadCorner)
|
||||
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else if chat.chatInfo.chatSettings?.favorite ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 18, height: 18)
|
||||
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
|
||||
.padding(.trailing, 1)
|
||||
.foregroundColor(.secondary.opacity(0.65))
|
||||
.foregroundColor(theme.colors.secondary.opacity(0.65))
|
||||
} else {
|
||||
Color.clear.frame(width: 0)
|
||||
}
|
||||
@@ -293,12 +300,12 @@ struct ChatPreviewView: View {
|
||||
let mc = ci.content.msgContent
|
||||
switch mc {
|
||||
case let .link(_, preview):
|
||||
smallContentPreview(
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 36, height: 36)
|
||||
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
|
||||
ZStack {
|
||||
Image(systemName: "arrow.up.right")
|
||||
.resizable()
|
||||
@@ -313,25 +320,25 @@ struct ChatPreviewView: View {
|
||||
.onTapGesture {
|
||||
UIApplication.shared.open(preview.uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
case let .image(_, image):
|
||||
smallContentPreview(
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
)
|
||||
}
|
||||
case let .video(_,image, duration):
|
||||
smallContentPreview(
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
)
|
||||
}
|
||||
case let .voice(_, duration):
|
||||
smallContentPreviewVoice(
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallView: true)
|
||||
)
|
||||
smallContentPreviewVoice(size: dynamicMediaSize) {
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallViewSize: dynamicMediaSize)
|
||||
}
|
||||
case .file:
|
||||
smallContentPreviewFile(
|
||||
CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallView: true)
|
||||
)
|
||||
smallContentPreviewFile(size: dynamicMediaSize) {
|
||||
CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize)
|
||||
}
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -365,74 +372,70 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatStatusImage() -> some View {
|
||||
let size = dynamicSize(userFont).incognitoSize
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
if contact.active && contact.activeConn != nil {
|
||||
switch (chatModel.contactNetworkStatus(contact)) {
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
default:
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
}
|
||||
case .group:
|
||||
if progressByTimeout {
|
||||
ProgressView()
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
}
|
||||
default:
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color) -> some View {
|
||||
@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color, size: CGFloat) -> some View {
|
||||
if incognito {
|
||||
Image(systemName: "theatermasks")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 22, height: 22)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(secondaryColor)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
func smallContentPreview(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(width: size, height: size)
|
||||
.cornerRadius(8)
|
||||
.overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))
|
||||
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true))
|
||||
.padding([.top, .leading], 3)
|
||||
.padding(.vertical, size / 6)
|
||||
.padding(.leading, 3)
|
||||
.offset(x: 6)
|
||||
}
|
||||
|
||||
func smallContentPreviewVoice(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(height: voiceMessageSizeBasedOnSquareSize(36))
|
||||
}
|
||||
func smallContentPreviewVoice(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(height: voiceMessageSizeBasedOnSquareSize(size))
|
||||
.padding(.vertical, size / 6)
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
|
||||
func smallContentPreviewFile(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
func smallContentPreviewFile(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(width: size, height: size)
|
||||
.padding(.vertical, size / 7)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ struct ContactConnectionView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@State private var localAlias = ""
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@State private var showContactConnectionInfo = false
|
||||
@@ -62,7 +63,7 @@ struct ContactConnectionView: View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Text(contactConnection.description)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
incognitoIcon(contactConnection.incognito, theme.colors.secondary)
|
||||
incognitoIcon(contactConnection.incognito, theme.colors.secondary, size: dynamicSize(userFont).incognitoSize)
|
||||
.padding(.top, 26)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ import SimpleXChat
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ChatInfoImage(chat: chat, size: 63)
|
||||
ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize)
|
||||
.padding(.leading, 4)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top) {
|
||||
|
||||
@@ -64,7 +64,7 @@ struct LocalAuthView: View {
|
||||
deleteAppDatabaseAndFiles()
|
||||
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
|
||||
m.chatId = nil
|
||||
m.reversedChatItems = []
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
m.chats = []
|
||||
m.users = []
|
||||
_ = kcAppPassword.set(password)
|
||||
|
||||
@@ -175,11 +175,6 @@
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64BAC45E2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */; };
|
||||
64BAC45F2C495205008D3995 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45A2C495205008D3995 /* libffi.a */; };
|
||||
64BAC4602C495205008D3995 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45B2C495205008D3995 /* libgmpxx.a */; };
|
||||
64BAC4612C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */; };
|
||||
64BAC4622C495205008D3995 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BAC45D2C495205008D3995 /* libgmp.a */; };
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
@@ -212,7 +207,6 @@
|
||||
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
|
||||
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
|
||||
CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; };
|
||||
CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; };
|
||||
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; };
|
||||
@@ -223,6 +217,12 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E50581062C3DDD9D009C3F71 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = E50581052C3DDD9D009C3F71 /* Yams */; };
|
||||
E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */; };
|
||||
E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */; };
|
||||
E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D22C56F7EF007928CC /* libffi.a */; };
|
||||
E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D32C56F7EF007928CC /* libgmp.a */; };
|
||||
E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D42C56F7EF007928CC /* libgmpxx.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -294,17 +294,6 @@
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CEE723D32C3C21C90009AE93 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
CEE723D02C3C21C90009AE93 /* SimpleXChat.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -524,11 +513,6 @@
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a"; sourceTree = "<group>"; };
|
||||
64BAC45A2C495205008D3995 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64BAC45B2C495205008D3995 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64BAC45D2C495205008D3995 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
@@ -568,6 +552,11 @@
|
||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a"; sourceTree = "<group>"; };
|
||||
E5DCF8D22C56F7EF007928CC /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5DCF8D32C56F7EF007928CC /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5DCF8D42C56F7EF007928CC /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -607,14 +596,22 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
64BAC4622C495205008D3995 /* libgmp.a in Frameworks */,
|
||||
64BAC45F2C495205008D3995 /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */,
|
||||
E50581062C3DDD9D009C3F71 /* Yams in Frameworks */,
|
||||
E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */,
|
||||
E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */,
|
||||
E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */,
|
||||
E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
64BAC4612C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a in Frameworks */,
|
||||
64BAC45E2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a in Frameworks */,
|
||||
64BAC4602C495205008D3995 /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E5DCF8DA2C56FABA007928CC /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -681,11 +678,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64BAC45A2C495205008D3995 /* libffi.a */,
|
||||
64BAC45D2C495205008D3995 /* libgmp.a */,
|
||||
64BAC45B2C495205008D3995 /* libgmpxx.a */,
|
||||
64BAC45C2C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU-ghc9.6.3.a */,
|
||||
64BAC4592C495205008D3995 /* libHSsimplex-chat-6.0.0.1-J5MWx9pYOGnDBWRfMkQxFU.a */,
|
||||
E5DCF8D22C56F7EF007928CC /* libffi.a */,
|
||||
E5DCF8D32C56F7EF007928CC /* libgmp.a */,
|
||||
E5DCF8D42C56F7EF007928CC /* libgmpxx.a */,
|
||||
E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */,
|
||||
E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1153,7 +1150,7 @@
|
||||
buildPhases = (
|
||||
CEE723A32C3BD3D70009AE93 /* Sources */,
|
||||
CEE723A52C3BD3D70009AE93 /* Resources */,
|
||||
CEE723D32C3C21C90009AE93 /* Embed Frameworks */,
|
||||
E5DCF8DA2C56FABA007928CC /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ public enum ChatCommand {
|
||||
case apiUnmuteUser(userId: Int64)
|
||||
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
|
||||
case startChat(mainApp: Bool, enableSndFiles: Bool)
|
||||
case checkChatRunning
|
||||
case apiStopChat
|
||||
case apiActivateChat(restoreChat: Bool)
|
||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||
@@ -173,6 +174,7 @@ public enum ChatCommand {
|
||||
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
|
||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
|
||||
case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))"
|
||||
case .checkChatRunning: return "/_check running"
|
||||
case .apiStopChat: return "/_stop"
|
||||
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||
@@ -334,6 +336,7 @@ public enum ChatCommand {
|
||||
case .apiUnmuteUser: return "apiUnmuteUser"
|
||||
case .apiDeleteUser: return "apiDeleteUser"
|
||||
case .startChat: return "startChat"
|
||||
case .checkChatRunning: return "checkChatRunning"
|
||||
case .apiStopChat: return "apiStopChat"
|
||||
case .apiActivateChat: return "apiActivateChat"
|
||||
case .apiSuspendChat: return "apiSuspendChat"
|
||||
|
||||
+16
-3
@@ -461,12 +461,11 @@ object ChatController {
|
||||
Log.d(TAG, "user: $user")
|
||||
try {
|
||||
apiSetNetworkConfig(getNetCfg())
|
||||
val justStarted = apiStartChat()
|
||||
appPrefs.chatStopped.set(false)
|
||||
val chatRunning = apiCheckChatRunning()
|
||||
val users = listUsers(null)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
if (justStarted) {
|
||||
if (!chatRunning) {
|
||||
chatModel.currentUser.value = user
|
||||
chatModel.localUserCreated.value = true
|
||||
getUserChatData(null)
|
||||
@@ -485,6 +484,8 @@ object ChatController {
|
||||
}
|
||||
Log.d(TAG, "startChat: running")
|
||||
}
|
||||
apiStartChat()
|
||||
appPrefs.chatStopped.set(false)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "failed starting chat $e")
|
||||
throw e
|
||||
@@ -738,6 +739,15 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun apiCheckChatRunning(): Boolean {
|
||||
val r = sendCmd(null, CC.CheckChatRunning())
|
||||
when (r) {
|
||||
is CR.ChatRunning -> return true
|
||||
is CR.ChatStopped -> return false
|
||||
else -> throw Exception("failed check chat running: ${r.responseType} ${r.details}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiStopChat(): Boolean {
|
||||
val r = sendCmd(null, CC.ApiStopChat())
|
||||
when (r) {
|
||||
@@ -2709,6 +2719,7 @@ sealed class CC {
|
||||
class ApiUnmuteUser(val userId: Long): CC()
|
||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||
class StartChat(val mainApp: Boolean): CC()
|
||||
class CheckChatRunning: CC()
|
||||
class ApiStopChat: CC()
|
||||
@Serializable
|
||||
class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC()
|
||||
@@ -2854,6 +2865,7 @@ sealed class CC {
|
||||
is ApiUnmuteUser -> "/_unmute user $userId"
|
||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||
is StartChat -> "/_start main=${onOff(mainApp)}"
|
||||
is CheckChatRunning -> "/_check running"
|
||||
is ApiStopChat -> "/_stop"
|
||||
is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}"
|
||||
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||
@@ -3007,6 +3019,7 @@ sealed class CC {
|
||||
is ApiUnmuteUser -> "apiUnmuteUser"
|
||||
is ApiDeleteUser -> "apiDeleteUser"
|
||||
is StartChat -> "startChat"
|
||||
is CheckChatRunning -> "checkChatRunning"
|
||||
is ApiStopChat -> "apiStopChat"
|
||||
is ApiSetAppFilePaths -> "apiSetAppFilePaths"
|
||||
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 2de16cfae89661605b468df71eff8b8e8188ef86
|
||||
tag: 83f8622b2397afe2635c8f60f1ec5f6fdc16ef7c
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 6.0.0.2
|
||||
version: 6.0.0.3
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."2de16cfae89661605b468df71eff8b8e8188ef86" = "00bgpy3gygqhmcbb2r5i8kryc5vn667bdg5s3xl3lf7y9m13g047";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."83f8622b2397afe2635c8f60f1ec5f6fdc16ef7c" = "1dn7b0pjlk32crp943l6lz4r376nf444kjfi167mrv1pgccri6ns";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.0.0.2
|
||||
version: 6.0.0.3
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
+282
-279
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,7 @@ import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWo
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
|
||||
@@ -264,6 +264,7 @@ data ChatCommand
|
||||
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
||||
| DeleteUser UserName Bool (Maybe UserPwd)
|
||||
| StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True
|
||||
| CheckChatRunning
|
||||
| APIStopChat
|
||||
| APIActivateChat {restoreChat :: Bool}
|
||||
| APISuspendChat {suspendTimeout :: Int}
|
||||
@@ -1393,11 +1394,24 @@ toView' ev = do
|
||||
|
||||
withStore' :: (DB.Connection -> IO a) -> CM a
|
||||
withStore' action = withStore $ liftIO . action
|
||||
{-# INLINE withStore' #-}
|
||||
|
||||
withFastStore' :: (DB.Connection -> IO a) -> CM a
|
||||
withFastStore' action = withFastStore $ liftIO . action
|
||||
{-# INLINE withFastStore' #-}
|
||||
|
||||
withStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a
|
||||
withStore action = do
|
||||
withStore = withStorePriority False
|
||||
{-# INLINE withStore #-}
|
||||
|
||||
withFastStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a
|
||||
withFastStore = withStorePriority True
|
||||
{-# INLINE withFastStore #-}
|
||||
|
||||
withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a
|
||||
withStorePriority priority action = do
|
||||
ChatController {chatStore} <- ask
|
||||
liftIOEither $ withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
|
||||
liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
|
||||
|
||||
withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a))
|
||||
withStoreBatch actions = do
|
||||
|
||||
@@ -648,6 +648,7 @@ testGroupSameName :: HasCallStack => FilePath -> IO ()
|
||||
testGroupSameName =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -1844,6 +1845,7 @@ testGroupLink :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLink =
|
||||
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -1947,6 +1949,7 @@ testGroupLinkDeleteGroupRejoin :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkDeleteGroupRejoin =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2003,6 +2006,7 @@ testGroupLinkContactUsed :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkContactUsed =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2151,6 +2155,7 @@ testGroupLinkUnusedHostContactDeleted =
|
||||
testChatCfg2 cfg aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
-- create group 1
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2285,6 +2290,7 @@ testGroupLinkMemberRole :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkMemberRole =
|
||||
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2420,6 +2426,7 @@ testPlanGroupLinkOkKnown :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkOkKnown =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2463,6 +2470,7 @@ testPlanHostContactDeletedGroupLinkKnown :: HasCallStack => FilePath -> IO ()
|
||||
testPlanHostContactDeletedGroupLinkKnown =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2569,14 +2577,15 @@ testPlanGroupLinkOwn tmp =
|
||||
testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkConnecting tmp = do
|
||||
-- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
|
||||
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice ##> "/create link #team"
|
||||
getGroupLink alice "team" GRMember True
|
||||
-- withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> ("/c " <> gLink)
|
||||
@@ -2591,14 +2600,14 @@ testPlanGroupLinkConnecting tmp = do
|
||||
|
||||
threadDelay 100000
|
||||
-- withTestChatCfg tmp cfg "alice" $ \alice -> do
|
||||
withTestChatCfg tmp cfg "alice" $ \a -> withTestOutput a $ \alice -> do
|
||||
withTestChatCfg tmp cfg "alice" $ \alice -> do
|
||||
alice
|
||||
<### [ "1 group links active",
|
||||
"#team: group is empty",
|
||||
"bob (Bob): accepting request to join group #team..."
|
||||
]
|
||||
-- withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
withTestChatCfg tmp cfg "bob" $ \b -> withTestOutput b $ \bob -> do
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
threadDelay 500000
|
||||
bob ##> ("/_connect plan 1 " <> gLink)
|
||||
bob <## "group link: connecting"
|
||||
@@ -2615,8 +2624,8 @@ testPlanGroupLinkConnecting tmp = do
|
||||
testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkLeaveRejoin =
|
||||
testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $
|
||||
-- \alice bob -> do
|
||||
\a b -> withTestOutput a $ \alice -> withTestOutput b $ \bob -> do
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2705,6 +2714,7 @@ testGroupLinkNoContact :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkNoContact =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -2928,6 +2938,7 @@ testGroupLinkNoContactMemberRole :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkNoContactMemberRole =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -3041,6 +3052,7 @@ testGroupLinkNoContactInviteeIncognito :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkNoContactInviteeIncognito =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -3145,6 +3157,7 @@ testPlanGroupLinkNoContactKnown :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkNoContactKnown =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -3180,6 +3193,7 @@ testPlanGroupLinkNoContactKnown =
|
||||
testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkNoContactConnecting tmp = do
|
||||
gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -3226,6 +3240,7 @@ testPlanGroupLinkNoContactConnecting tmp = do
|
||||
testPlanGroupLinkNoContactConnectingSlow :: HasCallStack => FilePath -> IO ()
|
||||
testPlanGroupLinkNoContactConnectingSlow tmp = do
|
||||
gLink <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -4123,6 +4138,7 @@ testMemberContactIncognito =
|
||||
testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
-- create group, bob joins incognito
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
@@ -5371,6 +5387,7 @@ testMembershipProfileUpdateNextGroupMessage =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
-- create group 1
|
||||
threadDelay 100000
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
|
||||
Reference in New Issue
Block a user