ios, android: direct messages in group - types, TODOs

This commit is contained in:
spaced4ndy 2023-09-08 19:07:04 +04:00
parent 281d9c7f79
commit 1682577ede
13 changed files with 193 additions and 81 deletions

View File

@ -315,11 +315,13 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r 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 chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) let cmd: ChatCommand = .apiSendMessage(sendRef: sendRef, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse let r: ChatResponse
if type == .direct { switch sendRef {
// TODO group-direct: re-use direct logic for directMember
case .direct:
var cItem: ChatItem? = nil var cItem: ChatItem? = nil
let endTask = beginBGTask({ let endTask = beginBGTask({
if let cItem = cItem { if let cItem = cItem {
@ -341,7 +343,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId:
} }
endTask() endTask()
return nil return nil
} else { default:
r = await chatSendCmd(cmd, bgDelay: msgDelay) r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(_, aChatItem) = r { if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem return aChatItem.chatItem

View File

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

View File

@ -33,7 +33,7 @@ struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample()) 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)) .previewLayout(.fixed(width: 360, height: 200))
} }

File diff suppressed because one or more lines are too long

View File

@ -428,7 +428,8 @@ struct ChatView: View {
} }
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir, // TODO group-direct: display message as sent/received privately
if case let .groupRcv(member, _) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo { case let .group(groupInfo) = chat.chatInfo {
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci) let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil { if ci.memberConnected != nil && nextItem?.memberConnected != nil {
@ -874,10 +875,11 @@ struct ChatView: View {
} }
} }
// TODO group-direct: show image if scope changes from group to private or vice versa
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) { switch (prevItem?.chatDir) {
case .groupSnd: return true case .groupSnd: return true
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId case let .groupRcv(prevMember, _): return prevMember.groupMemberId != member.groupMemberId
default: return false default: return false
} }
} }

View File

@ -747,25 +747,37 @@ struct ComposeView: View {
} }
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = await apiSendMessage( // TODO group-direct: directMember in compose state
type: chat.chatInfo.chatType, var sendRef: SendRef?
id: chat.chatInfo.apiId, let chatId = chat.chatInfo.apiId
file: file, switch chat.chatInfo.chatType {
quotedItemId: quoted, case .direct: sendRef = .direct(contactId: chatId)
msg: mc, case .group: sendRef = .group(groupId: chatId, directMemberId: nil)
live: live, default: sendRef = nil
ttl: ttl }
) { if let sendRef = sendRef {
await MainActor.run { if let chatItem = await apiSendMessage(
chatModel.removeLiveDummy(animated: false) sendRef: sendRef,
chatModel.addChatItem(chat.chatInfo, chatItem) file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
}
return chatItem
} }
return chatItem if let file = file {
removeFile(file.filePath)
}
return nil
} else {
logger.error("ComposeView send: sendRef is nil")
return nil
} }
if let file = file {
removeFile(file.filePath)
}
return nil
} }
func checkLinkPreview() -> MsgContent { func checkLinkPreview() -> MsgContent {

View File

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

View File

@ -1968,7 +1968,7 @@ public struct AChatItem: Decodable {
public var chatItem: ChatItem public var chatItem: ChatItem
public var chatId: String { public var chatId: String {
if case let .groupRcv(groupMember) = chatItem.chatDir { if case let .groupRcv(groupMember, _) = chatItem.chatDir {
return groupMember.id return groupMember.id
} }
return chatInfo.id return chatInfo.id
@ -2041,7 +2041,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberConnected: GroupMember? { public var memberConnected: GroupMember? {
switch chatDir { switch chatDir {
case .groupRcv(let groupMember): case let .groupRcv(groupMember, _):
switch content { switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil default: return nil
@ -2125,7 +2125,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberDisplayName: String? { public var memberDisplayName: String? {
get { get {
if case let .groupRcv(groupMember) = chatDir { if case let .groupRcv(groupMember, _) = chatDir {
return groupMember.displayName return groupMember.displayName
} else { } else {
return nil return nil
@ -2133,9 +2133,10 @@ public struct ChatItem: Identifiable, Decodable {
} }
} }
// TODO group-direct: prohibit moderation if directMember is present
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? {
switch (chatInfo, chatDir) { switch (chatInfo, chatDir) {
case let (.group(groupInfo), .groupRcv(groupMember)): case let (.group(groupInfo), .groupRcv(groupMember, _)):
let m = groupInfo.membership let m = groupInfo.membership
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
? (groupInfo, groupMember) ? (groupInfo, groupMember)
@ -2246,9 +2247,10 @@ public struct ChatItem: Identifiable, Decodable {
) )
} }
// TODO group-direct: possibly this has to take sendRef as parameter instead (for directMember)
public static func liveDummy(_ chatType: ChatType) -> ChatItem { public static func liveDummy(_ chatType: ChatType) -> ChatItem {
var item = ChatItem( var item = ChatItem(
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd, chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd(directMember: nil),
meta: CIMeta( meta: CIMeta(
itemId: -2, itemId: -2,
itemTs: .now, itemTs: .now,
@ -2280,11 +2282,34 @@ public struct ChatItem: Identifiable, Decodable {
} }
} }
public enum MessageScope: String, Decodable {
case msGroup = "group"
case msPrivate = "private"
}
public enum CIDirection: Decodable { public enum CIDirection: Decodable {
case directSnd case directSnd
case directRcv case directRcv
case groupSnd case groupSnd(directMember: GroupMember?)
case groupRcv(groupMember: 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 { public var sent: Bool {
get { get {
@ -2592,7 +2617,7 @@ public enum MsgDecryptError: String, Decodable {
} }
public struct CIQuote: Decodable, ItemContent { public struct CIQuote: Decodable, ItemContent {
public var chatDir: CIDirection? public var chatDir: CIQDirection?
public var itemId: Int64? public var itemId: Int64?
var sharedMsgId: String? = nil var sharedMsgId: String? = nil
public var sentAt: Date public var sentAt: Date
@ -2606,17 +2631,18 @@ public struct CIQuote: Decodable, ItemContent {
} }
} }
// TODO group-direct: "<sender> privately" possibly here?
public func getSender(_ membership: GroupMember?) -> String? { public func getSender(_ membership: GroupMember?) -> String? {
switch (chatDir) { switch (chatDir) {
case .directSnd: return "you" case .directSnd: return "you"
case .directRcv: return nil case .directRcv: return nil
case .groupSnd: return membership?.displayName ?? "you" case .groupSnd: return membership?.displayName ?? "you"
case let .groupRcv(member): return member.displayName case let .groupRcv(member, _): return member.displayName
case nil: return nil 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 let mc: MsgContent
if let image = image { if let image = image {
mc = .image(text: text, 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 { public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get() let previewMode = ntfPreviewModeGroupDefault.get()
var title: String 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) title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden)
} else { } else {
title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):" title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):"

View File

@ -1588,8 +1588,9 @@ data class ChatItem (
file = null file = null
) )
// TODO group-direct: possibly this has to take sendRef as parameter instead (for directMember)
fun liveDummy(direct: Boolean): ChatItem = ChatItem( 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( meta = CIMeta(
itemId = TEMP_LIVE_CHAT_ITEM_ID, itemId = TEMP_LIVE_CHAT_ITEM_ID,
itemTs = Clock.System.now(), itemTs = Clock.System.now(),
@ -1621,12 +1622,33 @@ data class ChatItem (
} }
} }
@Serializable
enum class MessageScope(val messageScope: String) {
@SerialName("group") MSGroup("group"),
@SerialName("private") MSPrivate("private");
}
@Serializable @Serializable
sealed class CIDirection { sealed class CIDirection {
@Serializable @SerialName("directSnd") class DirectSnd: CIDirection() @Serializable @SerialName("directSnd") class DirectSnd: CIDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIDirection() @Serializable @SerialName("directRcv") class DirectRcv: CIDirection()
@Serializable @SerialName("groupSnd") class GroupSnd: CIDirection() @Serializable @SerialName("groupSnd") class GroupSnd(val directMember: GroupMember? = null): CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): 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) { val sent: Boolean get() = when(this) {
is DirectSnd -> true is DirectSnd -> true
@ -1921,7 +1943,7 @@ enum class MsgDecryptError {
@Serializable @Serializable
class CIQuote ( class CIQuote (
val chatDir: CIDirection? = null, val chatDir: CIQDirection? = null,
val itemId: Long? = null, val itemId: Long? = null,
val sharedMsgId: String? = null, val sharedMsgId: String? = null,
val sentAt: Instant, val sentAt: Instant,
@ -1937,15 +1959,15 @@ class CIQuote (
fun sender(membership: GroupMember?): String? = when (chatDir) { fun sender(membership: GroupMember?): String? = when (chatDir) {
is CIDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun) is CIQDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.DirectRcv -> null is CIQDirection.DirectRcv -> null
is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun) is CIQDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.GroupRcv -> chatDir.groupMember.displayName is CIQDirection.GroupRcv -> chatDir.groupMember.displayName
null -> null null -> null
} }
companion object { 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)) CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text))
} }
} }

View File

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

View File

@ -1286,20 +1286,20 @@ fun PreviewGroupChatLayout() {
SimpleXTheme { SimpleXTheme {
val chatItems = listOf( val chatItems = listOf(
ChatItem.getSampleData( ChatItem.getSampleData(
1, CIDirection.GroupSnd(), Clock.System.now(), "hello" 1, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
), ),
ChatItem.getSampleData( 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.getDeletedContentSampleData(3),
ChatItem.getSampleData( ChatItem.getSampleData(
4, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello" 4, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
), ),
ChatItem.getSampleData( ChatItem.getSampleData(
5, CIDirection.GroupSnd(), Clock.System.now(), "hello" 5, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
), ),
ChatItem.getSampleData( 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 }) } 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? { suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage( // TODO group-direct: directMember in compose state
type = cInfo.chatType, val chatId = chat.chatInfo.apiId
id = cInfo.apiId, val sendRef = when (chat.chatInfo.chatType) {
file = file, ChatType.Direct -> SendRef.Direct(contactId = chatId)
quotedItemId = quoted, ChatType.Group -> SendRef.Group(groupId = chatId, directMemberId = null)
mc = mc, else -> null
live = live, }
ttl = ttl if (sendRef != null) {
) val aChatItem = chatModel.controller.apiSendMessage(
if (aChatItem != null) { sendRef = sendRef,
chatModel.addChatItem(cInfo, aChatItem.chatItem) file = file,
return aChatItem.chatItem 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? { suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val cs = composeState.value val cs = composeState.value