This commit is contained in:
spaced4ndy
2023-09-11 17:55:40 +04:00
parent 1682577ede
commit 77698a9883
8 changed files with 235 additions and 48 deletions
+4
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 {
+32 -25
View File
@@ -318,33 +318,15 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
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(sendRef: sendRef, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse
switch sendRef {
// TODO group-direct: re-use direct logic for directMember
case .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
default:
r = await chatSendCmd(cmd, bgDelay: msgDelay)
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
}
@@ -353,6 +335,31 @@ func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, m
}
}
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(
@@ -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
+68 -11
View File
@@ -207,6 +207,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,22 +435,27 @@ struct ChatView: View {
}
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
// TODO group-direct: display message as sent/received privately
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 == .msPrivate {
Text("\(member.displayName) **privately**")
} 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)
@@ -465,6 +477,38 @@ struct ChatView: View {
.padding(.leading, memberImageSize + 8 + 12)
}
}
} else if case let .groupSnd(directMember) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
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 {
ProfileImage(imageStr: directMember.image)
.frame(width: 30, height: 30)
.onTapGesture { selectedMember = directMember }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: directMember, navigation: true)
}
Text("**privately** to \(directMember.displayName)")
} 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)
@@ -875,11 +919,24 @@ 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, _ 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 true
case let .groupSnd(prevDirectMember):
switch (prevDirectMember, directMember) {
case (.none, .some(_)): return true
case (.some(_), .none): return true
case let (.some(prevDM), .some(dm)): return prevDM.groupMemberId != dm.groupMemberId
default: return false
}
default: return false
}
}
@@ -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
@@ -582,6 +598,23 @@ 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) }
)
case .directMemberCancelled:
ContextDirectMemberView(
directMember: nil,
cancelDirectMemberContext: { composeState = composeState.copy(directMember: .noDirectMember) }
)
}
}
@ViewBuilder private func contextItemView() -> some View {
switch composeState.contextItem {
case .noContextItem:
@@ -747,12 +780,18 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
// TODO group-direct: directMember in compose state
var sendRef: SendRef?
let chatId = chat.chatInfo.apiId
switch chat.chatInfo.chatType {
case .direct: sendRef = .direct(contactId: chatId)
case .group: sendRef = .group(groupId: chatId, directMemberId: nil)
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)
}
default: sendRef = nil
}
if let sendRef = sendRef {
@@ -0,0 +1,49 @@
//
// 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 {
ProfileImage(imageStr: directMember.image)
.frame(width: 30, height: 30)
.padding(.trailing, 2)
Text("Message will be sent privately to \(directMember.chatViewName)")
.lineLimit(1)
} else {
Text("Message will be sent to all 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: {})
}
}
@@ -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 */,
+35 -8
View File
@@ -2133,10 +2133,17 @@ public struct ChatItem: Identifiable, Decodable {
}
}
// TODO group-direct: prohibit moderation if directMember is present
public var directMember: GroupMember? {
switch chatDir {
case let .groupSnd(directMember): return directMember
case let .groupRcv(groupMember, .msPrivate): 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)
@@ -2588,12 +2595,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
}
@@ -2631,13 +2643,28 @@ public struct CIQuote: Decodable, ItemContent {
}
}
// TODO group-direct: "<sender> privately" possibly here?
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(.msPrivate):
if let membershipName = membership?.displayName {
return String.localizedStringWithFormat(NSLocalizedString("%@, privately", comment: "quote sender"), membershipName)
} else {
return NSLocalizedString("you, privately", comment: "quote sender")
}
case let .groupRcv(member, .msGroup):
return member.displayName
case let .groupRcv(member, .msPrivate):
return String.localizedStringWithFormat(NSLocalizedString("%@, privately", comment: "quote sender"), member.displayName)
case nil: return nil
}
}