Compare commits

...

10 Commits

Author SHA1 Message Date
Evgeny Poberezkin
b93c7759e1 update UI 2023-09-11 22:10:13 +01:00
spaced4ndy
2c56bc606b move avatar 2023-09-11 23:18:45 +04:00
spaced4ndy
a3cba562c6 wip 2023-09-11 22:08:14 +04:00
spaced4ndy
2c86b71329 rename 2023-09-11 21:52:36 +04:00
spaced4ndy
94fb94f06a wip 2023-09-11 21:50:34 +04:00
spaced4ndy
3cdba73109 wip 2023-09-11 21:04:00 +04:00
spaced4ndy
8efa023327 ms private -> direct 2023-09-11 18:45:36 +04:00
spaced4ndy
cf60183e74 Merge branch 'master' into f/group-direct-ui-types 2023-09-11 18:40:00 +04:00
spaced4ndy
77698a9883 wip 2023-09-11 17:55:40 +04:00
spaced4ndy
1682577ede ios, android: direct messages in group - types, TODOs 2023-09-08 19:07:04 +04:00
18 changed files with 603 additions and 129 deletions

View File

@@ -563,6 +563,10 @@ final class ChatModel: ObservableObject {
}
}
func getGroupMember(_ groupMemberId: Int64) -> GroupMember? {
groupMembers.first(where: { $0.groupMemberId == groupMemberId })
}
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
// user member was updated
if groupInfo.membership.groupMemberId == member.groupMemberId {

View File

@@ -315,34 +315,18 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
let chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse
if type == .direct {
var cItem: ChatItem? = nil
let endTask = beginBGTask({
if let cItem = cItem {
DispatchQueue.main.async {
chatModel.messageDelivery.removeValue(forKey: cItem.id)
}
}
})
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
} else {
sendMessageErrorAlert(r)
}
endTask()
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
let cmd: ChatCommand = .apiSendMessage(sendRef: sendRef, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
switch sendRef {
case .direct:
let cItem = await sendMessageDirect(cmd)
return cItem
case .group(_, .some(_)):
let cItem = await sendMessageDirect(cmd)
return cItem
case .group(_, .none):
let r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
}
@@ -351,6 +335,31 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId:
}
}
private func sendMessageDirect(_ cmd: ChatCommand) async -> ChatItem? {
let chatModel = ChatModel.shared
var cItem: ChatItem? = nil
let endTask = beginBGTask({
if let cItem = cItem {
DispatchQueue.main.async {
chatModel.messageDelivery.removeValue(forKey: cItem.id)
}
}
})
let r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
} else {
sendMessageErrorAlert(r)
}
endTask()
return nil
}
private func sendMessageErrorAlert(_ r: ChatResponse) {
logger.error("apiSendMessage error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(

View File

@@ -41,7 +41,7 @@ struct CIRcvDecryptionError: View {
.onAppear {
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
case let .groupRcv(groupMember, _) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
@@ -78,7 +78,7 @@ struct CIRcvDecryptionError: View {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
case let .groupRcv(groupMember, _) = chatItem.chatDir,
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
let memberStats = modelMember.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {

View File

@@ -33,7 +33,7 @@ struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData, messageScope: .msGroup)))
}
.previewLayout(.fixed(width: 360, height: 200))
}

File diff suppressed because one or more lines are too long

View File

@@ -290,7 +290,7 @@ struct ChatItemInfoView: View {
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
memberDeliveryStatuses.compactMap({ mds in
if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) {
if let mem = ChatModel.shared.getGroupMember(mds.groupMemberId) {
return (mem, mds.memberDeliveryStatus)
} else {
return nil

View File

@@ -141,6 +141,9 @@ struct ChatView: View {
.appSheet(isPresented: $showChatInfoSheet) {
GroupChatInfoView(chat: chat, groupInfo: groupInfo)
}
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -207,6 +210,13 @@ struct ChatView: View {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
} else if case let .group(groupInfo) = cInfo {
Task {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
}
}
}
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
@@ -428,29 +438,36 @@ struct ChatView: View {
}
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir,
if case let .groupRcv(member, msgScope) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
if prevItem == nil || showMemberImage(member, prevItem) {
if prevItem == nil || showMemberImage(member, msgScope, prevItem) {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Text(member.displayName)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
Group {
if msgScope == .msDirect {
Text("\(member.displayName) **only to you**")
} else {
Text(member.displayName)
}
}
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
}
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
RcvMemberImageWithMenu(
member: member,
groupInfo: groupInfo,
composeState: $composeState,
selectedMember: $selectedMember
)
.environmentObject(chat)
chatItemWithMenu(ci, maxWidth)
}
}
@@ -464,6 +481,34 @@ struct ChatView: View {
.padding(.leading, memberImageSize + 8 + 12)
}
}
} else if case let .groupSnd(directMember) = ci.chatDir {
let (prevItem, _) = chatModel.getChatItemNeighbors(ci)
if prevItem == nil || showSndScope(directMember, prevItem) {
VStack(alignment: .trailing, spacing: 4) {
if ci.content.showMemberName {
HStack {
if let directMember = directMember {
Text("**only to** \(directMember.displayName)")
ProfileImage(imageStr: directMember.image)
.frame(width: 20, height: 20)
.onTapGesture { selectedMember = directMember }
} else {
Text("to group")
}
}
.font(.caption)
.foregroundStyle(.secondary)
.padding(.top, 7)
}
chatItemWithMenu(ci, maxWidth)
}
.padding(.horizontal)
.padding(.top, 5)
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
}
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
@@ -471,6 +516,69 @@ struct ChatView: View {
}
}
private struct RcvMemberImageWithMenu: View {
var member: GroupMember
var groupInfo: GroupInfo
@Binding var composeState: ComposeState
@Binding var selectedMember: GroupMember?
@State private var allowMenu: Bool = true
var body: some View {
let menuItems = memberMenu(member, groupInfo)
if menuItems.isEmpty {
profileImage()
} else {
let uiMenu: Binding<UIMenu> = Binding(
get: { UIMenu(title: "", children: menuItems) },
set: { _ in }
)
profileImage()
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
}
}
private func profileImage() -> some View {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
}
private func memberMenu(_ member: GroupMember, _ groupInfo: GroupInfo) -> [UIMenuElement] {
var menu: [UIMenuElement] = []
if let memberWithConn = ChatModel.shared.getGroupMember(member.groupMemberId),
memberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
menu.append(memberMenuSendDirectlyUIAction(memberWithConn, groupInfo))
}
return menu
}
private func memberMenuSendDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
UIAction(
title: NSLocalizedString("Send directly", comment: "chat item action"),
image: UIImage(systemName: "arrow.left.arrow.right")
) { _ in
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
switch canSend {
case .notConnected:
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
case .notSupported:
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
case .canSend:
withAnimation {
if composeState.editing {
composeState = ComposeState(directMember: .directMember(groupMember: toDirectMember))
} else {
composeState = composeState.copy(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .noContextItem
)
}
}
}
}
}
}
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu(
ci: ci,
@@ -597,7 +705,15 @@ struct ChatView: View {
menu.append(rm)
}
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
menu.append(replyUIAction())
if ci.directMember == nil {
menu.append(replyUIAction())
}
if case let .group(groupInfo) = chat.chatInfo,
let toDirectMember = ci.memberToReplyDirectlyTo,
let toDirectMemberWithConn = ChatModel.shared.getGroupMember(toDirectMember.groupMemberId),
toDirectMemberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
menu.append(replyDirectlyUIAction(toDirectMemberWithConn, groupInfo))
}
}
menu.append(shareUIAction())
menu.append(copyUIAction())
@@ -646,13 +762,45 @@ struct ChatView: View {
private func replyUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("Reply", comment: "chat item action"),
image: UIImage(systemName: "arrowshape.turn.up.left")
image: UIImage(systemName: "arrowshape.turn.up.left.2")
) { _ in
withAnimation {
if composeState.editing {
composeState = ComposeState(contextItem: .quotedItem(chatItem: ci))
} else {
composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci))
composeState = composeState.copy(
directMember: .noDirectMember,
contextItem: .quotedItem(chatItem: ci)
)
}
}
}
}
private func replyDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
UIAction(
title: NSLocalizedString("Reply directly", comment: "chat item action"),
image: UIImage(systemName: "arrowshape.turn.up.left")
) { _ in
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
switch canSend {
case .notConnected:
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
case .notSupported:
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
case .canSend:
withAnimation {
if composeState.editing {
composeState = ComposeState(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .quotedItem(chatItem: ci)
)
} else {
composeState = composeState.copy(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .quotedItem(chatItem: ci)
)
}
}
}
}
@@ -874,10 +1022,33 @@ struct ChatView: View {
}
}
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
private static func cantSendDirectlyNotConnectedAlert() -> Alert {
Alert(
title: Text("Can't send directly!"),
message: Text("Member is not connected.")
)
}
private static func cantSendDirectlyNotSupportedAlert() -> Alert {
Alert(
title: Text("Can't send directly!"),
message: Text("Member doesn't support this feature, ask them to update.")
)
}
private func showMemberImage(_ member: GroupMember, _ msgScope: MessageScope, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) {
case .groupSnd: return true
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId
case let .groupRcv(prevMember, prevMsgScope): return prevMember.groupMemberId != member.groupMemberId || prevMsgScope != msgScope
default: return false
}
}
private func showSndScope(_ directMember: GroupMember?, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) {
case .groupRcv: return directMember != nil
case let .groupSnd(prevDirectMember):
return prevDirectMember?.groupMemberId != directMember?.groupMemberId
default: return false
}
}

View File

@@ -11,6 +11,12 @@ import SimpleXChat
import SwiftyGif
import PhotosUI
enum ComposeDirectMember {
case noDirectMember
case directMember(groupMember: GroupMember)
case directMemberCancelled
}
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview?)
@@ -40,6 +46,7 @@ struct LiveMessage {
struct ComposeState {
var message: String
var liveMessage: LiveMessage? = nil
var directMember: ComposeDirectMember
var preview: ComposePreview
var contextItem: ComposeContextItem
var voiceMessageRecordingState: VoiceMessageRecordingState
@@ -49,12 +56,14 @@ struct ComposeState {
init(
message: String = "",
liveMessage: LiveMessage? = nil,
directMember: ComposeDirectMember = .noDirectMember,
preview: ComposePreview = .noPreview,
contextItem: ComposeContextItem = .noContextItem,
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording
) {
self.message = message
self.liveMessage = liveMessage
self.directMember = directMember
self.preview = preview
self.contextItem = contextItem
self.voiceMessageRecordingState = voiceMessageRecordingState
@@ -62,6 +71,11 @@ struct ComposeState {
init(editingItem: ChatItem) {
self.message = editingItem.content.text
if let directMember = editingItem.directMember {
self.directMember = .directMember(groupMember: directMember)
} else {
self.directMember = .noDirectMember
}
self.preview = chatItemPreview(chatItem: editingItem)
self.contextItem = .editingItem(chatItem: editingItem)
if let emc = editingItem.content.msgContent,
@@ -75,6 +89,7 @@ struct ComposeState {
func copy(
message: String? = nil,
liveMessage: LiveMessage? = nil,
directMember: ComposeDirectMember? = nil,
preview: ComposePreview? = nil,
contextItem: ComposeContextItem? = nil,
voiceMessageRecordingState: VoiceMessageRecordingState? = nil
@@ -82,6 +97,7 @@ struct ComposeState {
ComposeState(
message: message ?? self.message,
liveMessage: liveMessage ?? self.liveMessage,
directMember: directMember ?? self.directMember,
preview: preview ?? self.preview,
contextItem: contextItem ?? self.contextItem,
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState
@@ -257,6 +273,7 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
contextDirectMemberView()
contextItemView()
switch (composeState.editing, composeState.preview) {
case (true, .filePreview): EmptyView()
@@ -582,6 +599,26 @@ struct ComposeView: View {
}
}
@ViewBuilder private func contextDirectMemberView() -> some View {
switch composeState.directMember {
case .noDirectMember:
EmptyView()
case let .directMember(groupMember):
ContextDirectMemberView(
directMember: groupMember,
cancelDirectMemberContext: { composeState = composeState.copy(
directMember: .directMemberCancelled,
contextItem: .noContextItem
)}
)
case .directMemberCancelled:
ContextDirectMemberView(
directMember: nil,
cancelDirectMemberContext: { composeState = composeState.copy(directMember: .noDirectMember) }
)
}
}
@ViewBuilder private func contextItemView() -> some View {
switch composeState.contextItem {
case .noContextItem:
@@ -747,25 +784,43 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
var sendRef: SendRef?
let chatId = chat.chatInfo.apiId
switch chat.chatInfo.chatType {
case .direct:
sendRef = .direct(contactId: chatId)
case .group:
switch composeState.directMember {
case let .directMember(groupMember):
sendRef = .group(groupId: chatId, directMemberId: groupMember.groupMemberId)
default:
sendRef = .group(groupId: chatId, directMemberId: nil)
}
return chatItem
default: sendRef = nil
}
if let file = file {
removeFile(file.filePath)
if let sendRef = sendRef {
if let chatItem = await apiSendMessage(
sendRef: sendRef,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
}
return chatItem
}
if let file = file {
removeFile(file.filePath)
}
return nil
} else {
logger.error("ComposeView send: sendRef is nil")
return nil
}
return nil
}
func checkLinkPreview() -> MsgContent {

View File

@@ -0,0 +1,59 @@
//
// ContextDirectMemberView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 11.09.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContextDirectMemberView: View {
@Environment(\.colorScheme) var colorScheme
let directMember: GroupMember?
let cancelDirectMemberContext: () -> Void
var body: some View {
HStack {
if let directMember = directMember {
Text("**only to** \(directMember.chatViewName)")
.lineLimit(1)
if let image = directMember.image {
ProfileImage(imageStr: image)
.frame(width: 24, height: 24)
.padding(.trailing, 2)
} else {
Image(systemName: "arrow.left.arrow.right")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(.secondary)
.padding(.trailing, 2)
}
} else {
// maybe make it disappear after some time?
Text("send to all group members")
}
Spacer()
Button {
withAnimation {
cancelDirectMemberContext()
}
} label: {
Image(systemName: "multiply")
}
}
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.padding(.top, 8)
}
}
struct ContextDirectMemberView_Previews: PreviewProvider {
static var previews: some View {
ContextDirectMemberView(directMember: GroupMember.sampleData, cancelDirectMemberContext: {})
}
}

View File

@@ -239,7 +239,7 @@ struct GroupMemberInfoView: View {
chatModel.chatId = chat.id
}
} label: {
Label("Send direct message", systemImage: "message")
Label("Open direct chat", systemImage: "message")
}
}
@@ -256,7 +256,7 @@ struct GroupMemberInfoView: View {
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
}
} label: {
Label("Send direct message", systemImage: "message")
Label("Open direct chat", systemImage: "message")
}
}

View File

@@ -172,6 +172,7 @@
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64A2723E2AAF16CD00BE1136 /* ContextDirectMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
@@ -449,6 +450,7 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextDirectMemberView.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>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
@@ -837,6 +839,7 @@
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */,
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */,
);
path = ComposeMessage;
sourceTree = "<group>";
@@ -1119,6 +1122,7 @@
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
64A2723E2AAF16CD00BE1136 /* ContextDirectMemberView.swift in Sources */,
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */,
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,

View File

@@ -39,7 +39,7 @@ public enum ChatCommand {
case apiGetChats(userId: Int64)
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
@@ -156,10 +156,10 @@ public enum ChatCommand {
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
(search == "" ? "" : " search=\(search)")
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
case let .apiSendMessage(sendRef, file, quotedItemId, mc, live, ttl):
let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc))
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
return "/_send \(sendRefStr(sendRef)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
@@ -365,6 +365,14 @@ public enum ChatCommand {
"\(type.rawValue)\(id)"
}
func sendRefStr(_ sendRef: SendRef) -> String {
switch sendRef {
case let .direct(contactId): return "@\(contactId)"
case let .group(groupId, .none): return "#\(groupId)"
case let .group(groupId, .some(directMemberId)): return "#\(groupId) @\(directMemberId)"
}
}
func protoServersStr(_ servers: [ServerCfg]) -> String {
encodeJSON(ProtoServersConfig(servers: servers))
}
@@ -825,6 +833,11 @@ public enum ChatResponse: Decodable, Error {
}
}
public enum SendRef {
case direct(contactId: Int64)
case group(groupId: Int64, directMemberId: Int64?)
}
public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
switch chatResponse {
case let .chatCmdError(_, .error(error)): return error
@@ -1454,6 +1467,7 @@ public enum ChatErrorType: Decodable {
case agentCommandError(message: String)
case invalidFileDescription(message: String)
case connectionIncognitoChangeProhibited
case peerChatVRangeIncompatible
case internalError(message: String)
case exception(message: String)
}
@@ -1468,6 +1482,7 @@ public enum StoreError: Decodable {
case userNotFoundByContactRequestId(contactRequestId: Int64)
case contactNotFound(contactId: Int64)
case contactNotFoundByName(contactName: ContactName)
case contactNotFoundByMemberId(groupMemberId: Int64)
case contactNotReady(contactName: ContactName)
case duplicateContactLink
case userContactLinkNotFound
@@ -1495,6 +1510,7 @@ public enum StoreError: Decodable {
case rcvFileNotFoundXFTP(agentRcvFileId: String)
case connectionNotFound(agentConnId: String)
case connectionNotFoundById(connId: Int64)
case connectionNotFoundByMemberId(groupMemberId: Int64)
case pendingConnectionNotFound(connId: Int64)
case introNotFound
case uniqueID

View File

@@ -1449,6 +1449,7 @@ public struct ContactSubStatus: Decodable {
public struct Connection: Decodable {
public var connId: Int64
public var agentConnId: String
// public var peerChatVRange: VersionRange
var connStatus: ConnStatus
public var connLevel: Int
public var viaGroupLink: Bool
@@ -1459,6 +1460,7 @@ public struct Connection: Decodable {
private enum CodingKeys: String, CodingKey {
case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
// case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
}
public var id: ChatId { get { ":\(connId)" } }
@@ -1466,12 +1468,22 @@ public struct Connection: Decodable {
static let sampleData = Connection(
connId: 1,
agentConnId: "abc",
// peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1),
connStatus: .ready,
connLevel: 0,
viaGroupLink: false
)
}
public struct VersionRange: Decodable {
public var minVersion: Int
public var maxVersion: Int
public func isCompatibleRange(_ range: VersionRange) -> Bool {
minVersion <= range.maxVersion && range.minVersion <= maxVersion
}
}
public struct SecurityCode: Decodable, Equatable {
public init(securityCode: String, verifiedAt: Date) {
self.securityCode = securityCode
@@ -1807,6 +1819,30 @@ public struct GroupMember: Identifiable, Decodable {
return GroupMemberRole.allCases.filter { $0 <= userRole }
}
public func allowedToSendDirectlyTo(groupInfo: GroupInfo) -> Bool {
let userRole = groupInfo.membership.memberRole
return (
(groupInfo.fullGroupPreferences.directMessages.on && userRole >= .author)
|| userRole >= .admin
|| memberRole >= .admin
)
}
public enum CanSendDirectlyTo {
case canSend
case notConnected
case notSupported
}
public func canSendDirectlyTo(groupInfo: GroupInfo) -> CanSendDirectlyTo {
// TODO group-direct: check version
if let activeConn = activeConn {
return .canSend
} else {
return .notConnected
}
}
public var memberIncognito: Bool {
memberProfile.profileId != memberContactProfileId
}
@@ -1834,6 +1870,7 @@ public struct GroupMemberRef: Decodable {
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer"
case author = "author"
case member = "member"
case admin = "admin"
case owner = "owner"
@@ -1843,6 +1880,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
public var text: String {
switch self {
case .observer: return NSLocalizedString("observer", comment: "member role")
case .author: return NSLocalizedString("author", comment: "member role")
case .member: return NSLocalizedString("member", comment: "member role")
case .admin: return NSLocalizedString("admin", comment: "member role")
case .owner: return NSLocalizedString("owner", comment: "member role")
@@ -1852,9 +1890,10 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
private var comparisonValue: Int {
switch self {
case .observer: return 0
case .member: return 1
case .admin: return 2
case .owner: return 3
case .author: return 1
case .member: return 2
case .admin: return 3
case .owner: return 4
}
}
@@ -1968,7 +2007,7 @@ public struct AChatItem: Decodable {
public var chatItem: ChatItem
public var chatId: String {
if case let .groupRcv(groupMember) = chatItem.chatDir {
if case let .groupRcv(groupMember, _) = chatItem.chatDir {
return groupMember.id
}
return chatInfo.id
@@ -2041,7 +2080,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberConnected: GroupMember? {
switch chatDir {
case .groupRcv(let groupMember):
case let .groupRcv(groupMember, _):
switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil
@@ -2125,7 +2164,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberDisplayName: String? {
get {
if case let .groupRcv(groupMember) = chatDir {
if case let .groupRcv(groupMember, _) = chatDir {
return groupMember.displayName
} else {
return nil
@@ -2133,9 +2172,25 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public var directMember: GroupMember? {
switch chatDir {
case let .groupSnd(directMember): return directMember
case let .groupRcv(groupMember, .msDirect): return groupMember
default: return nil
}
}
public var memberToReplyDirectlyTo: GroupMember? {
switch chatDir {
case let .groupSnd(directMember): return directMember
case let .groupRcv(groupMember, _): return groupMember
default: return nil
}
}
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? {
switch (chatInfo, chatDir) {
case let (.group(groupInfo), .groupRcv(groupMember)):
case let (.group(groupInfo), .groupRcv(groupMember, .msGroup)):
let m = groupInfo.membership
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
? (groupInfo, groupMember)
@@ -2248,7 +2303,7 @@ public struct ChatItem: Identifiable, Decodable {
public static func liveDummy(_ chatType: ChatType) -> ChatItem {
var item = ChatItem(
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd,
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd(directMember: nil),
meta: CIMeta(
itemId: -2,
itemTs: .now,
@@ -2280,11 +2335,34 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public enum MessageScope: String, Decodable {
case msGroup = "group"
case msDirect = "direct"
}
public enum CIDirection: Decodable {
case directSnd
case directRcv
case groupSnd
case groupRcv(groupMember: GroupMember)
case groupSnd(directMember: GroupMember?)
case groupRcv(groupMember: GroupMember, messageScope: MessageScope)
public var sent: Bool {
get {
switch self {
case .directSnd: return true
case .directRcv: return false
case .groupSnd: return true
case .groupRcv: return false
}
}
}
}
public enum CIQDirection: Decodable {
case directSnd
case directRcv
case groupSnd(messageScope: MessageScope)
case groupRcv(groupMember: GroupMember, messageScope: MessageScope)
public var sent: Bool {
get {
@@ -2563,12 +2641,17 @@ public enum CIContent: Decodable, ItemContent {
public var showMemberName: Bool {
switch self {
case .rcvMsgContent: return true
case .sndMsgContent: return true
case .rcvDeleted: return true
case .sndDeleted: return true
case .rcvCall: return true
case .sndCall: return true
case .rcvIntegrityError: return true
case .rcvDecryptionError: return true
case .rcvGroupInvitation: return true
case .sndGroupInvitation: return true
case .rcvModerated: return true
case .sndModerated: return true
case .invalidJSON: return true
default: return false
}
@@ -2592,7 +2675,7 @@ public enum MsgDecryptError: String, Decodable {
}
public struct CIQuote: Decodable, ItemContent {
public var chatDir: CIDirection?
public var chatDir: CIQDirection?
public var itemId: Int64?
var sharedMsgId: String? = nil
public var sentAt: Date
@@ -2607,16 +2690,35 @@ public struct CIQuote: Decodable, ItemContent {
}
public func getSender(_ membership: GroupMember?) -> String? {
switch (chatDir) {
case .directSnd: return "you"
case .directRcv: return nil
case .groupSnd: return membership?.displayName ?? "you"
case let .groupRcv(member): return member.displayName
switch chatDir {
case .directSnd:
return NSLocalizedString("you", comment: "quote sender")
case .directRcv:
return nil
case .groupSnd(.msGroup):
if let membershipName = membership?.displayName {
return membershipName
} else {
return NSLocalizedString("you", comment: "quote sender")
}
case .groupSnd(.msDirect):
// TODO don't quite understand this condition - this is always user's message, why we show the name, and when "you" will be shown?
// Maybe better to repeat "only to <member>" as it was on the original message?
// Or "<user name> only to <member>"?
if let membershipName = membership?.displayName {
return String.localizedStringWithFormat(NSLocalizedString("%@, directly", comment: "quote sender"), membershipName)
} else {
return NSLocalizedString("you, directly", comment: "quote sender")
}
case let .groupRcv(member, .msGroup):
return member.displayName
case let .groupRcv(member, .msDirect):
return String.localizedStringWithFormat(NSLocalizedString("%@ only to you", comment: "quote sender"), member.displayName)
case nil: return nil
}
}
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIQDirection?, image: String? = nil) -> CIQuote {
let mc: MsgContent
if let image = image {
mc = .image(text: text, image: image)

View File

@@ -59,7 +59,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact)
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get()
var title: String
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember, _) = cItem.chatDir {
title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden)
} else {
title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):"

View File

@@ -1596,8 +1596,9 @@ data class ChatItem (
file = null
)
// TODO group-direct: possibly this has to take sendRef as parameter instead (for directMember)
fun liveDummy(direct: Boolean): ChatItem = ChatItem(
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(),
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(directMember = null),
meta = CIMeta(
itemId = TEMP_LIVE_CHAT_ITEM_ID,
itemTs = Clock.System.now(),
@@ -1629,12 +1630,33 @@ data class ChatItem (
}
}
@Serializable
enum class MessageScope(val messageScope: String) {
@SerialName("group") MSGroup("group"),
@SerialName("direct") MSDirect("direct");
}
@Serializable
sealed class CIDirection {
@Serializable @SerialName("directSnd") class DirectSnd: CIDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIDirection()
@Serializable @SerialName("groupSnd") class GroupSnd: CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection()
@Serializable @SerialName("groupSnd") class GroupSnd(val directMember: GroupMember? = null): CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember, val messageScope: MessageScope): CIDirection()
val sent: Boolean get() = when(this) {
is DirectSnd -> true
is DirectRcv -> false
is GroupSnd -> true
is GroupRcv -> false
}
}
@Serializable
sealed class CIQDirection {
@Serializable @SerialName("directSnd") class DirectSnd: CIQDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIQDirection()
@Serializable @SerialName("groupSnd") class GroupSnd(val messageScope: MessageScope): CIQDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember, val messageScope: MessageScope): CIQDirection()
val sent: Boolean get() = when(this) {
is DirectSnd -> true
@@ -1929,7 +1951,7 @@ enum class MsgDecryptError {
@Serializable
class CIQuote (
val chatDir: CIDirection? = null,
val chatDir: CIQDirection? = null,
val itemId: Long? = null,
val sharedMsgId: String? = null,
val sentAt: Instant,
@@ -1945,15 +1967,15 @@ class CIQuote (
fun sender(membership: GroupMember?): String? = when (chatDir) {
is CIDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.DirectRcv -> null
is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.GroupRcv -> chatDir.groupMember.displayName
is CIQDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIQDirection.DirectRcv -> null
is CIQDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
is CIQDirection.GroupRcv -> chatDir.groupMember.displayName
null -> null
}
companion object {
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote =
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIQDirection?): CIQuote =
CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text))
}
}

View File

@@ -587,8 +587,8 @@ object ChatController {
return null
}
suspend fun apiSendMessage(type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
suspend fun apiSendMessage(sendRef: SendRef, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(sendRef, file, quotedItemId, mc, live, ttl)
val r = sendCmd(cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
@@ -1805,7 +1805,7 @@ sealed class CC {
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiSendMessage(val sendRef: SendRef, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
@@ -1909,7 +1909,7 @@ sealed class CC {
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiSendMessage -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
"/_send ${sendRefStr(sendRef)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
}
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
@@ -2105,10 +2105,27 @@ sealed class CC {
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun sendRefStr(sendRef: SendRef): String {
return when (sendRef) {
is SendRef.Direct -> "@${sendRef.contactId}"
is SendRef.Group -> {
when (sendRef.directMemberId) {
null -> "#${sendRef.groupId}"
else -> "#${sendRef.groupId} @${sendRef.directMemberId}"
}
}
}
}
fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers))
}
}
sealed class SendRef {
class Direct(val contactId: Long): SendRef()
class Group(val groupId: Long, val directMemberId: Long?): SendRef()
}
@Serializable
data class NewUser(
val profile: Profile?,
@@ -3820,6 +3837,7 @@ sealed class ChatErrorType {
is AgentCommandError -> "agentCommandError"
is InvalidFileDescription -> "invalidFileDescription"
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is InternalError -> "internalError"
is CEException -> "exception $message"
}
@@ -3894,6 +3912,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
}
@@ -3911,6 +3930,7 @@ sealed class StoreError {
is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId"
is ContactNotFound -> "contactNotFound"
is ContactNotFoundByName -> "contactNotFoundByName"
is ContactNotFoundByMemberId -> "contactNotFoundByMemberId"
is ContactNotReady -> "contactNotReady"
is DuplicateContactLink -> "duplicateContactLink"
is UserContactLinkNotFound -> "userContactLinkNotFound"
@@ -3938,6 +3958,7 @@ sealed class StoreError {
is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP"
is ConnectionNotFound -> "connectionNotFound"
is ConnectionNotFoundById -> "connectionNotFoundById"
is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId"
is PendingConnectionNotFound -> "pendingConnectionNotFound"
is IntroNotFound -> "introNotFound"
is UniqueID -> "uniqueID"
@@ -3966,6 +3987,7 @@ sealed class StoreError {
@Serializable @SerialName("userNotFoundByContactRequestId") class UserNotFoundByContactRequestId(val contactRequestId: Long): StoreError()
@Serializable @SerialName("contactNotFound") class ContactNotFound(val contactId: Long): StoreError()
@Serializable @SerialName("contactNotFoundByName") class ContactNotFoundByName(val contactName: String): StoreError()
@Serializable @SerialName("contactNotFoundByMemberId") class ContactNotFoundByMemberId(val groupMemberId: Long): StoreError()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contactName: String): StoreError()
@Serializable @SerialName("duplicateContactLink") object DuplicateContactLink: StoreError()
@Serializable @SerialName("userContactLinkNotFound") object UserContactLinkNotFound: StoreError()
@@ -3993,6 +4015,7 @@ sealed class StoreError {
@Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError()
@Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError()
@Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError()
@Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError()
@Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError()
@Serializable @SerialName("introNotFound") object IntroNotFound: StoreError()
@Serializable @SerialName("uniqueID") object UniqueID: StoreError()

View File

@@ -1286,20 +1286,20 @@ fun PreviewGroupChatLayout() {
SimpleXTheme {
val chatItems = listOf(
ChatItem.getSampleData(
1, CIDirection.GroupSnd(), Clock.System.now(), "hello"
1, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
2, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
2, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
),
ChatItem.getDeletedContentSampleData(3),
ChatItem.getSampleData(
4, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
4, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
5, CIDirection.GroupSnd(), Clock.System.now(), "hello"
5, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
6, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
)
)
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }

View File

@@ -318,25 +318,34 @@ fun ComposeView(
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
// TODO group-direct: directMember in compose state
val chatId = chat.chatInfo.apiId
val sendRef = when (chat.chatInfo.chatType) {
ChatType.Direct -> SendRef.Direct(contactId = chatId)
ChatType.Group -> SendRef.Group(groupId = chatId, directMemberId = null)
else -> null
}
if (sendRef != null) {
val aChatItem = chatModel.controller.apiSendMessage(
sendRef = sendRef,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(file.filePath)
return null
} else {
Log.e(TAG, "ComposeView send: sendRef is null")
return null
}
if (file != null) removeFile(file.filePath)
return null
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value