2022-01-29 11:10:04 +00:00
//
// C h a t V i e w . s w i f t
// S i m p l e X
//
// C r e a t e d b y E v g e n y P o b e r e z k i n o n 2 7 / 0 1 / 2 0 2 2 .
// C o p y r i g h t © 2 0 2 2 S i m p l e X C h a t . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
2022-05-31 07:55:13 +01:00
import SimpleXChat
2022-12-24 00:22:12 +03:00
import SwiftyGif
2024-07-03 10:24:26 +01:00
import Combine
2022-01-29 11:10:04 +00:00
2022-03-30 08:57:42 +01:00
private let memberImageSize : CGFloat = 34
2022-01-29 11:10:04 +00:00
struct ChatView : View {
@ EnvironmentObject var chatModel : ChatModel
2024-07-29 23:17:14 +03:00
@ ObservedObject var im = ItemsModel . shared
2024-07-03 22:42:13 +01:00
@ State var theme : AppTheme = buildTheme ( )
2023-01-23 18:17:33 +00:00
@ Environment ( \ . dismiss ) var dismiss
2024-07-03 22:42:13 +01:00
@ Environment ( \ . colorScheme ) var colorScheme
2023-02-06 19:33:45 +03:00
@ Environment ( \ . presentationMode ) var presentationMode
2024-03-29 19:43:16 +00:00
@ Environment ( \ . scenePhase ) var scenePhase
2022-12-30 21:47:11 +04:00
@ State @ ObservedObject var chat : Chat
2024-07-03 10:24:26 +01:00
@ StateObject private var scrollModel = ReverseListScrollModel < ChatItem > ( )
@ StateObject private var floatingButtonModel = FloatingButtonModel ( )
2022-07-27 11:16:07 +04:00
@ State private var showChatInfoSheet : Bool = false
@ State private var showAddMembersSheet : Bool = false
2022-05-07 06:40:46 +01:00
@ State private var composeState = ComposeState ( )
2023-07-10 13:53:46 +01:00
@ State private var keyboardVisible = false
2022-08-02 14:48:31 +04:00
@ State private var connectionStats : ConnectionStats ?
2022-08-23 18:18:12 +04:00
@ State private var customUserProfile : Profile ?
2022-12-12 08:59:35 +00:00
@ State private var connectionCode : String ?
2022-08-15 21:07:11 +01:00
@ State private var loadingItems = false
@ State private var firstPage = false
2024-07-03 10:24:26 +01:00
@ State private var revealedChatItem : ChatItem ?
2022-08-17 11:43:18 +01:00
@ State private var searchMode = false
@ State private var searchText : String = " "
@ FocusState private var searchFocussed
2022-08-26 17:27:38 +04:00
// o p e n i n g G r o u p M e m b e r I n f o V i e w o n m e m b e r i c o n
2023-10-31 09:44:57 +00:00
@ State private var selectedMember : GMember ? = nil
2023-10-26 18:51:45 +04:00
// o p e n i n g G r o u p L i n k V i e w o n l i n k b u t t o n ( i n c o g n i t o )
@ State private var showGroupLinkSheet : Bool = false
@ State private var groupLink : String ?
@ State private var groupLinkMemberRole : GroupMemberRole = . member
2024-07-31 23:00:14 +09:00
@ State private var selectedChatItems : Set < Int64 > ? = nil
@ State private var showDeleteSelectedMessages : Bool = false
@ State private var allowToDeleteSelectedMessagesForAll : Bool = false
2022-12-12 08:59:35 +00:00
2024-08-09 17:26:11 +01:00
@ AppStorage ( DEFAULT_TOOLBAR_MATERIAL ) private var toolbarMaterial = ToolbarMaterial . defaultMaterial
2024-08-09 19:23:57 +03:00
2022-01-29 11:10:04 +00:00
var body : some View {
2023-07-10 13:53:46 +01:00
if #available ( iOS 16.0 , * ) {
2024-08-09 19:23:57 +03:00
viewBody
. scrollDismissesKeyboard ( . immediately )
. toolbarBackground ( . hidden , for : . navigationBar )
2023-07-10 13:53:46 +01:00
} else {
viewBody
}
}
2024-08-13 21:37:48 +03:00
2024-08-09 19:23:57 +03:00
@ ViewBuilder
2023-07-10 13:53:46 +01:00
private var viewBody : some View {
2022-02-08 09:19:25 +00:00
let cInfo = chat . chatInfo
2024-08-09 19:23:57 +03:00
ZStack {
let wallpaperImage = theme . wallpaper . type . image
let wallpaperType = theme . wallpaper . type
let backgroundColor = theme . wallpaper . background ? ? wallpaperType . defaultBackgroundColor ( theme . base , theme . colors . background )
let tintColor = theme . wallpaper . tint ? ? wallpaperType . defaultTintColor ( theme . base )
Color . clear . ignoresSafeArea ( . all )
. if ( wallpaperImage != nil ) { view in
view . modifier (
ChatViewBackground ( image : wallpaperImage ! , imageType : wallpaperType , background : backgroundColor , tint : tintColor )
)
2022-08-17 11:43:18 +01:00
}
2024-08-09 19:23:57 +03:00
VStack ( spacing : 0 ) {
ZStack ( alignment : . bottomTrailing ) {
chatItemsList ( )
floatingButtons ( counts : floatingButtonModel . unreadChatItemCounts )
2024-07-03 22:42:13 +01:00
}
2024-08-09 19:23:57 +03:00
connectingText ( )
if selectedChatItems = = nil {
ComposeView (
chat : chat ,
composeState : $ composeState ,
keyboardVisible : $ keyboardVisible
)
. disabled ( ! cInfo . sendMsgEnabled )
} else {
SelectedItemsBottomToolbar (
chatItems : ItemsModel . shared . reversedChatItems ,
selectedChatItems : $ selectedChatItems ,
chatInfo : chat . chatInfo ,
deleteItems : { forAll in
allowToDeleteSelectedMessagesForAll = forAll
showDeleteSelectedMessages = true
} ,
moderateItems : {
if case let . group ( groupInfo ) = chat . chatInfo {
showModerateSelectedMessagesAlert ( groupInfo )
}
2024-07-31 23:00:14 +09:00
}
2024-08-09 19:23:57 +03:00
)
}
}
}
. safeAreaInset ( edge : . top ) {
VStack ( spacing : . zero ) {
if searchMode { searchToolbar ( ) }
Divider ( )
2024-07-31 23:00:14 +09:00
}
2024-08-09 19:23:57 +03:00
. background ( ToolbarMaterial . material ( toolbarMaterial ) )
2022-01-29 11:10:04 +00:00
}
2022-02-08 09:19:25 +00:00
. navigationTitle ( cInfo . chatViewName )
2024-07-03 22:42:13 +01:00
. background ( theme . colors . background )
2022-02-05 20:10:47 +00:00
. navigationBarTitleDisplayMode ( . inline )
2024-07-03 22:42:13 +01:00
. environmentObject ( theme )
2024-07-31 23:00:14 +09:00
. confirmationDialog ( selectedChatItems ? . count = = 1 ? " Delete message? " : " Delete \( ( selectedChatItems ? . count ? ? 0 ) ) messages? " , isPresented : $ showDeleteSelectedMessages , titleVisibility : . visible ) {
Button ( " Delete for me " , role : . destructive ) {
if let selected = selectedChatItems {
deleteMessages ( chat , selected . sorted ( ) , . cidmInternal , moderate : false , deletedSelectedMessages ) }
}
if allowToDeleteSelectedMessagesForAll {
Button ( broadcastDeleteButtonText ( chat ) , role : . destructive ) {
if let selected = selectedChatItems {
allowToDeleteSelectedMessagesForAll = false
deleteMessages ( chat , selected . sorted ( ) , . cidmBroadcast , moderate : false , deletedSelectedMessages )
}
}
}
}
2024-08-13 21:37:48 +03:00
. appSheet ( item : $ selectedMember ) { member in
Group {
if case let . group ( groupInfo ) = chat . chatInfo {
GroupMemberInfoView ( groupInfo : groupInfo , groupMember : member , navigation : true )
}
}
}
2022-10-21 12:32:11 +01:00
. onAppear {
2024-07-31 23:00:14 +09:00
selectedChatItems = nil
2024-08-13 21:37:48 +03:00
initChatView ( )
2022-10-21 12:32:11 +01:00
}
2023-07-10 19:01:22 +04:00
. onChange ( of : chatModel . chatId ) { cId in
2024-04-16 12:28:39 +04:00
showChatInfoSheet = false
2024-08-13 21:37:48 +03:00
selectedChatItems = nil
scrollModel . scrollToBottom ( )
2024-07-24 00:11:42 +07:00
stopAudioPlayer ( )
2024-04-16 12:28:39 +04:00
if let cId {
if let c = chatModel . getChat ( cId ) {
2024-08-11 14:06:25 +03:00
chat = c
2024-04-16 12:28:39 +04:00
}
2023-07-10 19:01:22 +04:00
initChatView ( )
2024-07-03 22:42:13 +01:00
theme = buildTheme ( )
2023-07-10 19:01:22 +04:00
} else {
dismiss ( )
}
2023-01-24 19:24:46 +00:00
}
2024-07-03 10:24:26 +01:00
. onChange ( of : revealedChatItem ) { _ in
NotificationCenter . postReverseListNeedsLayout ( )
}
2024-08-13 21:37:48 +03:00
. onChange ( of : im . isLoading ) { isLoading in
if ! isLoading ,
im . reversedChatItems . count <= loadItemsPerPage ,
filtered ( im . reversedChatItems ) . count < 10 {
2024-07-03 10:24:26 +01:00
loadChatItems ( chat . chatInfo )
}
}
. environmentObject ( scrollModel )
2023-01-24 19:24:46 +00:00
. onDisappear {
2023-04-06 20:26:48 +03:00
VideoPlayerView . players . removeAll ( )
2024-07-24 00:11:42 +07:00
stopAudioPlayer ( )
2023-02-06 19:33:45 +03:00
if chatModel . chatId = = cInfo . id && ! presentationMode . wrappedValue . isPresented {
2023-01-24 19:24:46 +00:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.35 ) {
if chatModel . chatId = = nil {
2023-10-18 11:23:35 +01:00
chatModel . chatItemStatuses = [ : ]
2024-07-29 23:17:14 +03:00
ItemsModel . shared . reversedChatItems = [ ]
2023-10-31 09:44:57 +00:00
chatModel . groupMembers = [ ]
2024-07-10 16:15:14 +04:00
chatModel . groupMembersIndexes . removeAll ( )
2024-07-11 10:57:56 +03:00
chatModel . membersLoaded = false
2022-08-29 14:08:46 +01:00
}
2022-02-02 12:51:39 +00:00
}
2022-02-01 17:34:06 +00:00
}
2023-01-24 19:24:46 +00:00
}
2024-07-03 22:42:13 +01:00
. onChange ( of : colorScheme ) { _ in
theme = buildTheme ( )
}
2023-01-24 19:24:46 +00:00
. toolbar {
2022-02-05 20:10:47 +00:00
ToolbarItem ( placement : . principal ) {
2024-07-31 23:00:14 +09:00
if selectedChatItems != nil {
SelectedItemsTopToolbar ( selectedChatItems : $ selectedChatItems )
} else if case let . direct ( contact ) = cInfo {
2022-08-26 17:27:38 +04:00
Button {
2022-08-02 14:48:31 +04:00
Task {
2024-08-05 12:58:24 +01:00
showChatInfoSheet = true
2022-08-02 14:48:31 +04:00
}
2022-08-26 17:27:38 +04:00
} label : {
ChatInfoToolbar ( chat : chat )
}
2024-08-11 21:11:11 +09:00
. appSheet ( isPresented : $ showChatInfoSheet , onDismiss : { theme = buildTheme ( ) } ) {
2024-08-05 12:58:24 +01:00
ChatInfoView (
chat : chat ,
contact : contact ,
localAlias : chat . chatInfo . localAlias ,
onSearch : { focusSearch ( ) }
)
2022-08-26 17:27:38 +04:00
}
} else if case let . group ( groupInfo ) = cInfo {
Button {
2024-07-11 10:57:56 +03:00
Task { await chatModel . loadGroupMembers ( groupInfo ) { showChatInfoSheet = true } }
2022-08-26 17:27:38 +04:00
} label : {
ChatInfoToolbar ( chat : chat )
2024-07-03 22:42:13 +01:00
. tint ( theme . colors . primary )
2022-08-02 14:48:31 +04:00
}
2024-07-03 22:42:13 +01:00
. appSheet ( isPresented : $ showChatInfoSheet , onDismiss : { theme = buildTheme ( ) } ) {
2023-10-31 09:44:57 +00:00
GroupChatInfoView (
chat : chat ,
groupInfo : Binding (
get : { groupInfo } ,
set : { gInfo in
chat . chatInfo = . group ( groupInfo : gInfo )
chat . created = Date . now
}
2024-08-05 12:58:24 +01:00
) ,
onSearch : { focusSearch ( ) }
2023-10-31 09:44:57 +00:00
)
2022-07-14 16:40:32 +04:00
}
2024-01-18 22:57:14 +07:00
} else if case . local = cInfo {
ChatInfoToolbar ( chat : chat )
2022-02-05 20:10:47 +00:00
}
}
2022-05-24 19:34:27 +01:00
ToolbarItem ( placement : . navigationBarTrailing ) {
2024-08-13 21:37:48 +03:00
let isLoading = im . isLoading && im . showLoadingProgress
2024-07-31 23:00:14 +09:00
if selectedChatItems != nil {
Button {
withAnimation {
selectedChatItems = nil
2023-04-18 10:29:49 +02:00
}
2024-07-31 23:00:14 +09:00
} label : {
Text ( " Cancel " )
}
} else {
switch cInfo {
case let . direct ( contact ) :
HStack {
let callsPrefEnabled = contact . mergedPreferences . calls . enabled . forUser
if callsPrefEnabled {
if chatModel . activeCall = = nil {
callButton ( contact , . audio , imageName : " phone " )
. disabled ( ! contact . ready || ! contact . active )
} else if let call = chatModel . activeCall , call . contact . id = = cInfo . id {
endCallButton ( call )
2023-04-18 10:29:49 +02:00
}
2022-08-17 11:43:18 +01:00
}
2024-07-31 23:00:14 +09:00
Menu {
2024-08-13 21:37:48 +03:00
if ! isLoading {
if callsPrefEnabled && chatModel . activeCall = = nil {
Button {
CallController . shared . startCall ( contact , . video )
} label : {
Label ( " Video call " , systemImage : " video " )
}
. disabled ( ! contact . ready || ! contact . active )
2022-08-29 14:47:29 +04:00
}
2024-08-13 21:37:48 +03:00
searchButton ( )
ToggleNtfsButton ( chat : chat )
. disabled ( ! contact . ready || ! contact . active )
2024-07-31 23:00:14 +09:00
}
} label : {
Image ( systemName : " ellipsis " )
2024-08-13 21:37:48 +03:00
. tint ( isLoading ? Color . clear : nil )
. overlay { if isLoading { ProgressView ( ) } }
2022-08-29 14:47:29 +04:00
}
2022-08-17 11:43:18 +01:00
}
2024-07-31 23:00:14 +09:00
case let . group ( groupInfo ) :
HStack {
if groupInfo . canAddMembers {
if ( chat . chatInfo . incognito ) {
groupLinkButton ( )
. appSheet ( isPresented : $ showGroupLinkSheet ) {
GroupLinkView (
groupId : groupInfo . groupId ,
groupLink : $ groupLink ,
groupLinkMemberRole : $ groupLinkMemberRole ,
showTitle : true ,
creatingGroup : false
)
}
} else {
addMembersButton ( )
. appSheet ( isPresented : $ showAddMembersSheet ) {
AddGroupMembersView ( chat : chat , groupInfo : groupInfo )
}
}
}
Menu {
2024-08-13 21:37:48 +03:00
if ! isLoading {
searchButton ( )
ToggleNtfsButton ( chat : chat )
}
2024-07-31 23:00:14 +09:00
} label : {
Image ( systemName : " ellipsis " )
2024-08-13 21:37:48 +03:00
. tint ( isLoading ? Color . clear : nil )
. overlay { if isLoading { ProgressView ( ) } }
2024-07-31 23:00:14 +09:00
}
2022-08-17 11:43:18 +01:00
}
2024-07-31 23:00:14 +09:00
case . local :
searchButton ( )
default :
EmptyView ( )
2022-07-30 13:03:44 +01:00
}
2022-05-24 19:34:27 +01:00
}
}
2022-02-01 17:34:06 +00:00
}
2022-01-29 11:10:04 +00:00
}
2024-07-11 10:57:56 +03:00
2023-07-10 19:01:22 +04:00
private func initChatView ( ) {
let cInfo = chat . chatInfo
2024-03-29 19:43:16 +00:00
// T h i s c h e c k p r e v e n t s t h e c a l l t o a p i C o n t a c t I n f o a f t e r t h e a p p i s s u s p e n d e d , a n d t h e d a t a b a s e i s c l o s e d .
if case . active = scenePhase ,
case let . direct ( contact ) = cInfo {
2023-07-10 19:01:22 +04:00
Task {
do {
let ( stats , _ ) = try await apiContactInfo ( chat . chatInfo . apiId )
await MainActor . run {
if let s = stats {
chatModel . updateContactConnectionStats ( contact , s )
}
}
} catch let error {
logger . error ( " apiContactInfo error: \( responseError ( error ) ) " )
}
}
}
2024-04-16 12:28:39 +04:00
if chatModel . draftChatId = = cInfo . id && ! composeState . forwarding ,
let draft = chatModel . draft {
2023-07-10 19:01:22 +04:00
composeState = draft
}
if chat . chatStats . unreadChat {
Task {
await markChatUnread ( chat , unreadChat : false )
}
}
}
2022-08-17 11:43:18 +01:00
private func searchToolbar ( ) -> some View {
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
HStack ( spacing : 12 ) {
HStack ( spacing : 4 ) {
2022-08-17 11:43:18 +01:00
Image ( systemName : " magnifyingglass " )
TextField ( " Search " , text : $ searchText )
2022-12-03 15:53:46 +04:00
. focused ( $ searchFocussed )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . onBackground )
2022-12-03 15:53:46 +04:00
. frame ( maxWidth : . infinity )
2023-06-19 11:49:45 +01:00
2022-08-17 11:43:18 +01:00
Button {
searchText = " "
} label : {
Image ( systemName : " xmark.circle.fill " ) . opacity ( searchText = = " " ? 0 : 1 )
}
}
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
. padding ( EdgeInsets ( top : 7 , leading : 7 , bottom : 7 , trailing : 7 ) )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
ios: rework UX of creating new connection (#3482)
* ios: connection UI (wip)
* custom search
* rework invite
* connect paste link ui
* scan rework, process errors, other fixes
* scan layout
* clear link on cancel
* improved search
* further improve search
* animation
* connect on paste in search
* layout
* layout
* layout
* layout, add conn
* delete unused invitation, create used invitation chat
* remove old views
* regular paste button
* new chat menu
* previews
* increase spacing
* animation, fix alerts
* swipe
* change text
* less sensitive gesture
* layout
* search cancel button transition
* slow down chat list animation (uses deprecated modifiers)
* icons
* update code scanner, layout
* manage camera permissions
* ask to delete unused invitation
* comment
* remove onDismiss
* don't filter chats on link in search, allow to paste text with link
* cleanup link after connection
* filter chat by link
* revert change
* show link descr
* disabled search
* underline
* filter own group
* simplify
* no animation
* add delay, move createInvitation
* update library
* possible fix for ios 15
* add explicit frame to qr code
* update library
* Revert "add explicit frame to qr code"
This reverts commit 95c7d31e47b3da39b5985cd57638885c45b77de1.
* remove comment
* fix pasteboardHasURLs, disable paste button based on it
* align help texts with changed button names
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
* update library
* Revert "fix pasteboardHasURLs, disable paste button based on it"
This reverts commit 46f63572e90dbf460faab9ce694181209712bd00.
* remove unused var
* restore disabled
* export localizations
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com>
2023-12-29 16:29:49 +04:00
. background ( Color ( . tertiarySystemFill ) )
2022-08-17 11:43:18 +01:00
. cornerRadius ( 10.0 )
2023-06-19 11:49:45 +01:00
2022-08-17 11:43:18 +01:00
Button ( " Cancel " ) {
searchText = " "
searchMode = false
searchFocussed = false
2024-08-13 21:37:48 +03:00
Task { await loadChat ( chat : chat ) }
2022-08-17 11:43:18 +01:00
}
}
. padding ( . horizontal )
. padding ( . vertical , 8 )
}
2024-07-15 13:14:14 +01:00
2023-05-14 20:07:34 +03:00
private func voiceWithoutFrame ( _ ci : ChatItem ) -> Bool {
2024-04-12 12:56:09 +04:00
ci . content . msgContent ? . isVoice = = true && ci . content . text . count = = 0 && ci . quotedItem = = nil && ci . meta . itemForwarded = = nil
2023-05-14 20:07:34 +03:00
}
2024-07-03 10:24:26 +01:00
private func filtered ( _ reversedChatItems : Array < ChatItem > ) -> Array < ChatItem > {
reversedChatItems
. enumerated ( )
. filter { ( index , chatItem ) in
2024-08-04 20:48:54 +01:00
if let mergeCategory = chatItem . mergeCategory , index > 0 {
2024-07-03 10:24:26 +01:00
mergeCategory != reversedChatItems [ index - 1 ] . mergeCategory
} else {
true
}
}
. map { $0 . element }
}
2024-07-03 22:42:13 +01:00
2022-08-17 11:43:18 +01:00
private func chatItemsList ( ) -> some View {
let cInfo = chat . chatInfo
2024-07-29 23:17:14 +03:00
let mergedItems = filtered ( im . reversedChatItems )
2022-08-17 11:43:18 +01:00
return GeometryReader { g in
2024-07-03 10:24:26 +01:00
ReverseList ( items : mergedItems , scrollState : $ scrollModel . state ) { ci in
let voiceNoFrame = voiceWithoutFrame ( ci )
let maxWidth = cInfo . chatType = = . group
? voiceNoFrame
? ( g . size . width - 28 ) - 42
: ( g . size . width - 28 ) * 0.84 - 42
: voiceNoFrame
? ( g . size . width - 32 )
: ( g . size . width - 32 ) * 0.84
2024-08-11 14:06:25 +03:00
return ChatItemWithMenu (
chat : $ chat ,
chatItem : ci ,
maxWidth : maxWidth ,
composeState : $ composeState ,
selectedMember : $ selectedMember ,
revealedChatItem : $ revealedChatItem ,
selectedChatItems : $ selectedChatItems
)
. onAppear {
floatingButtonModel . appeared ( viewId : ci . viewId )
}
. onDisappear {
floatingButtonModel . disappeared ( viewId : ci . viewId )
}
. id ( ci . id ) // R e q u i r e d t o t r i g g e r ` o n A p p e a r ` o n i O S 1 5
2024-07-03 10:24:26 +01:00
} loadPage : {
loadChatItems ( cInfo )
}
2024-08-13 21:37:48 +03:00
. opacity ( ItemsModel . shared . isLoading ? 0 : 1 )
2024-08-09 19:23:57 +03:00
. padding ( . vertical , - InvertedTableView . inset )
. onTapGesture { hideKeyboard ( ) }
. onChange ( of : searchText ) { _ in
2024-08-13 21:37:48 +03:00
Task { await loadChat ( chat : chat , search : searchText ) }
2024-08-09 19:23:57 +03:00
}
. onChange ( of : im . reversedChatItems ) { _ in
floatingButtonModel . chatItemsChanged ( )
}
2024-08-11 12:07:45 +03:00
. onChange ( of : im . itemAdded ) { added in
if added {
im . itemAdded = false
if floatingButtonModel . unreadChatItemCounts . isReallyNearBottom {
scrollModel . scrollToBottom ( )
}
}
}
2022-08-16 13:13:29 +01:00
}
}
2023-09-20 12:26:16 +04:00
@ ViewBuilder private func connectingText ( ) -> some View {
if case let . direct ( contact ) = chat . chatInfo ,
2024-07-19 11:31:43 +04:00
! contact . sndReady ,
2023-09-27 20:07:32 +04:00
contact . active ,
2023-09-20 12:26:16 +04:00
! contact . nextSendGrpInv {
Text ( " connecting… " )
. font ( . caption )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . secondary )
2023-09-20 12:26:16 +04:00
. padding ( . top )
} else {
EmptyView ( )
}
}
2024-07-03 10:24:26 +01:00
class FloatingButtonModel : ObservableObject {
private enum Event {
case appeared ( String )
case disappeared ( String )
case chatItemsChanged
}
@ Published var unreadChatItemCounts : UnreadChatItemCounts
private let events = PassthroughSubject < Event , Never > ( )
private var bag = Set < AnyCancellable > ( )
init ( ) {
unreadChatItemCounts = UnreadChatItemCounts (
2024-07-05 14:34:03 +02:00
isNearBottom : true ,
2024-08-11 12:07:45 +03:00
isReallyNearBottom : true ,
2024-08-04 20:48:54 +01:00
unreadBelow : 0
2024-07-03 10:24:26 +01:00
)
events
. receive ( on : DispatchQueue . global ( qos : . background ) )
. scan ( Set < String > ( ) ) { itemsInView , event in
2024-08-11 12:07:45 +03:00
var updated = itemsInView
switch event {
case let . appeared ( viewId ) : updated . insert ( viewId )
case let . disappeared ( viewId ) : updated . remove ( viewId )
case . chatItemsChanged : ( )
2024-07-03 10:24:26 +01:00
}
2024-08-11 12:07:45 +03:00
return updated
2024-07-03 10:24:26 +01:00
}
. map { ChatModel . shared . unreadChatItemCounts ( itemsInView : $0 ) }
. removeDuplicates ( )
2024-07-05 14:34:03 +02:00
. throttle ( for : . seconds ( 0.2 ) , scheduler : DispatchQueue . main , latest : true )
2024-07-03 10:24:26 +01:00
. assign ( to : \ . unreadChatItemCounts , on : self )
. store ( in : & bag )
}
func appeared ( viewId : String ) {
events . send ( . appeared ( viewId ) )
}
func disappeared ( viewId : String ) {
events . send ( . disappeared ( viewId ) )
}
func chatItemsChanged ( ) {
events . send ( . chatItemsChanged )
}
}
private func floatingButtons ( counts : UnreadChatItemCounts ) -> some View {
VStack {
2022-08-16 13:13:29 +01:00
let unreadAbove = chat . chatStats . unreadCount - counts . unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText ( unreadAbove )
. font ( . callout )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . primary )
2022-08-16 13:13:29 +01:00
}
2024-07-03 10:24:26 +01:00
. onTapGesture {
scrollModel . scrollToNextPage ( )
}
2022-08-16 13:13:29 +01:00
. contextMenu {
Button {
2024-07-03 10:24:26 +01:00
Task {
await markChatRead ( chat )
2022-08-16 13:13:29 +01:00
}
} label : {
Label ( " Mark read " , systemImage : " checkmark " )
}
}
}
Spacer ( )
if counts . unreadBelow > 0 {
circleButton {
unreadCountText ( counts . unreadBelow )
. font ( . callout )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . primary )
2022-08-16 13:13:29 +01:00
}
2024-07-03 10:24:26 +01:00
. onTapGesture {
2024-07-11 10:57:56 +03:00
scrollModel . scrollToBottom ( )
2024-07-03 10:24:26 +01:00
}
2024-07-05 14:34:03 +02:00
} else if ! counts . isNearBottom {
2022-08-16 13:13:29 +01:00
circleButton {
Image ( systemName : " chevron.down " )
2024-07-03 22:42:13 +01:00
. foregroundColor ( theme . colors . primary )
2022-08-16 13:13:29 +01:00
}
2024-07-03 10:24:26 +01:00
. onTapGesture { scrollModel . scrollToBottom ( ) }
2022-08-16 13:13:29 +01:00
}
}
. padding ( )
}
2024-07-03 10:24:26 +01:00
2022-08-16 13:13:29 +01:00
private func circleButton < Content : View > ( _ content : @ escaping ( ) -> Content ) -> some View {
ZStack {
Circle ( )
. foregroundColor ( Color ( uiColor : . tertiarySystemGroupedBackground ) )
. frame ( width : 44 , height : 44 )
content ( )
}
}
2024-07-03 10:24:26 +01:00
2022-05-07 06:40:46 +01:00
private func callButton ( _ contact : Contact , _ media : CallMediaType , imageName : String ) -> some View {
Button {
2022-05-24 19:34:27 +01:00
CallController . shared . startCall ( contact , media )
2022-05-07 06:40:46 +01:00
} label : {
Image ( systemName : imageName )
}
}
2024-02-13 22:04:42 +07:00
private func endCallButton ( _ call : Call ) -> some View {
Button {
if let uuid = call . callkitUUID {
CallController . shared . endCall ( callUUID : uuid )
} else {
CallController . shared . endCall ( call : call ) { }
}
} label : {
Image ( systemName : " phone.down.fill " ) . tint ( . red )
}
}
2022-08-17 11:43:18 +01:00
private func searchButton ( ) -> some View {
Button {
2024-08-05 12:58:24 +01:00
focusSearch ( )
2022-08-17 11:43:18 +01:00
} label : {
Label ( " Search " , systemImage : " magnifyingglass " )
}
}
2024-07-03 10:24:26 +01:00
2024-08-05 12:58:24 +01:00
private func focusSearch ( ) {
searchMode = true
searchFocussed = true
searchText = " "
}
2022-07-26 12:33:10 +04:00
private func addMembersButton ( ) -> some View {
2022-07-26 10:55:58 +04:00
Button {
2022-07-30 13:03:44 +01:00
if case let . group ( gInfo ) = chat . chatInfo {
2024-07-11 10:57:56 +03:00
Task { await chatModel . loadGroupMembers ( gInfo ) { showAddMembersSheet = true } }
2022-07-30 13:03:44 +01:00
}
2022-07-26 10:55:58 +04:00
} label : {
Image ( systemName : " person.crop.circle.badge.plus " )
}
}
2023-10-26 18:51:45 +04:00
private func groupLinkButton ( ) -> some View {
Button {
if case let . group ( gInfo ) = chat . chatInfo {
Task {
do {
if let link = try apiGetGroupLink ( gInfo . groupId ) {
( groupLink , groupLinkMemberRole ) = link
}
} catch let error {
logger . error ( " ChatView apiGetGroupLink: \( responseError ( error ) ) " )
}
showGroupLinkSheet = true
}
}
} label : {
Image ( systemName : " link.badge.plus " )
}
}
2024-07-31 23:00:14 +09:00
private func showModerateSelectedMessagesAlert ( _ groupInfo : GroupInfo ) {
guard let count = selectedChatItems ? . count , count > 0 else { return }
AlertManager . shared . showAlert ( Alert (
title : Text ( count = = 1 ? " Delete member message? " : " Delete \( count ) messages of members? " ) ,
message : Text (
groupInfo . fullGroupPreferences . fullDelete . on
? ( count = = 1 ? " The message will be deleted for all members. " : " The messages will be deleted for all members. " )
: ( count = = 1 ? " The message will be marked as moderated for all members. " : " The messages will be marked as moderated for all members. " )
) ,
primaryButton : . destructive ( Text ( " Delete " ) ) {
if let selected = selectedChatItems {
deleteMessages ( chat , selected . sorted ( ) , . cidmBroadcast , moderate : true , deletedSelectedMessages )
}
} ,
secondaryButton : . cancel ( )
) )
}
private func deletedSelectedMessages ( ) async {
await MainActor . run {
withAnimation {
selectedChatItems = nil
}
}
}
2024-07-03 10:24:26 +01:00
private func loadChatItems ( _ cInfo : ChatInfo ) {
Task {
2022-08-16 13:13:29 +01:00
if loadingItems || firstPage { return }
2022-08-15 21:07:11 +01:00
loadingItems = true
2024-07-03 10:24:26 +01:00
do {
var reversedPage = Array < ChatItem > ( )
var chatItemsAvailable = true
// L o a d a d d i t i o n a l i t e m s u n t i l t h e p a g e i s + 5 0 l a r g e a f t e r m e r g i n g
while chatItemsAvailable && filtered ( reversedPage ) . count < loadItemsPerPage {
let pagination : ChatPagination =
2024-07-29 23:17:14 +03:00
if let lastItem = reversedPage . last ? ? im . reversedChatItems . last {
2024-07-03 10:24:26 +01:00
. before ( chatItemId : lastItem . id , count : loadItemsPerPage )
} else {
. last ( count : loadItemsPerPage )
}
let chatItems = try await apiGetChatItems (
2022-08-15 21:07:11 +01:00
type : cInfo . chatType ,
id : cInfo . apiId ,
2024-07-03 10:24:26 +01:00
pagination : pagination ,
2022-08-17 11:43:18 +01:00
search : searchText
2022-08-15 21:07:11 +01:00
)
2024-07-03 10:24:26 +01:00
chatItemsAvailable = ! chatItems . isEmpty
reversedPage . append ( contentsOf : chatItems . reversed ( ) )
}
await MainActor . run {
if reversedPage . count = = 0 {
firstPage = true
} else {
2024-07-29 23:17:14 +03:00
im . reversedChatItems . append ( contentsOf : reversedPage )
2022-08-15 21:07:11 +01:00
}
2024-07-03 10:24:26 +01:00
loadingItems = false
2022-08-15 21:07:11 +01:00
}
2024-07-03 10:24:26 +01:00
} catch let error {
logger . error ( " apiGetChat error: \( responseError ( error ) ) " )
await MainActor . run { loadingItems = false }
2022-08-15 21:07:11 +01:00
}
}
}
2024-07-03 10:24:26 +01:00
2024-07-24 00:11:42 +07:00
func stopAudioPlayer ( ) {
VoiceItemState . chatView . values . forEach { $0 . audioPlayer ? . stop ( ) }
VoiceItemState . chatView = [ : ]
}
2023-10-31 09:44:57 +00:00
private struct ChatItemWithMenu : View {
@ EnvironmentObject var m : ChatModel
2024-07-03 22:42:13 +01:00
@ EnvironmentObject var theme : AppTheme
2024-08-11 14:06:25 +03:00
@ Binding @ ObservedObject var chat : Chat
2024-08-25 21:21:24 +03:00
@ ObservedObject var dummyModel : ChatItemDummyModel = . shared
2024-07-11 10:57:56 +03:00
let chatItem : ChatItem
let maxWidth : CGFloat
2023-10-31 09:44:57 +00:00
@ Binding var composeState : ComposeState
@ Binding var selectedMember : GMember ?
2024-07-03 10:24:26 +01:00
@ Binding var revealedChatItem : ChatItem ?
2023-10-31 09:44:57 +00:00
@ State private var deletingItem : ChatItem ? = nil
@ State private var showDeleteMessage = false
@ State private var deletingItems : [ Int64 ] = [ ]
@ State private var showDeleteMessages = false
@ State private var showChatItemInfoSheet : Bool = false
@ State private var chatItemInfo : ChatItemInfo ?
2024-04-16 12:28:39 +04:00
@ State private var showForwardingSheet : Bool = false
2023-10-31 09:44:57 +00:00
2024-07-31 23:00:14 +09:00
@ Binding var selectedChatItems : Set < Int64 > ?
2023-10-31 09:44:57 +00:00
@ State private var allowMenu : Bool = true
2024-07-03 10:24:26 +01:00
var revealed : Bool { chatItem = = revealedChatItem }
2024-08-25 21:21:24 +03:00
typealias ItemSeparation = ( timestamp : Bool , largeGap : Bool )
func getItemSeparation ( _ chatItem : ChatItem , at i : Int ? ) -> ItemSeparation {
let im = ItemsModel . shared
if let i , i > 0 && im . reversedChatItems . count >= i {
let nextItem = im . reversedChatItems [ i - 1 ]
let largeGap = ! nextItem . chatDir . sameDirection ( chatItem . chatDir ) || nextItem . meta . createdAt . timeIntervalSince ( chatItem . meta . createdAt ) > 60
return (
timestamp : largeGap || formatTimestampText ( chatItem . meta . createdAt ) != formatTimestampText ( nextItem . meta . createdAt ) ,
largeGap : largeGap
)
} else {
return ( timestamp : true , largeGap : true )
}
}
2023-10-31 09:44:57 +00:00
var body : some View {
2024-08-25 21:21:24 +03:00
let currIndex = m . getChatItemIndex ( chatItem )
2023-10-31 09:44:57 +00:00
let ciCategory = chatItem . mergeCategory
2024-07-03 10:24:26 +01:00
let ( prevHidden , prevItem ) = m . getPrevShownChatItem ( currIndex , ciCategory )
let range = itemsRange ( currIndex , prevHidden )
2024-08-25 21:21:24 +03:00
let timeSeparation = getItemSeparation ( chatItem , at : currIndex )
2024-07-29 23:17:14 +03:00
let im = ItemsModel . shared
2024-07-03 10:24:26 +01:00
Group {
2023-10-31 09:44:57 +00:00
if revealed , let range = range {
2024-07-29 23:17:14 +03:00
let items = Array ( zip ( Array ( range ) , im . reversedChatItems [ range ] ) )
2024-08-25 21:21:24 +03:00
VStack ( spacing : 0 ) {
ForEach ( items . reversed ( ) , id : \ . 1. viewId ) { ( i : Int , ci : ChatItem ) in
let prev = i = = prevHidden ? prevItem : im . reversedChatItems [ i + 1 ]
chatItemView ( ci , nil , prev , getItemSeparation ( ci , at : i ) )
. overlay {
if let selected = selectedChatItems , ci . canBeDeletedForSelf {
Color . clear
. contentShape ( Rectangle ( ) )
. onTapGesture {
let checked = selected . contains ( ci . id )
selectUnselectChatItem ( select : ! checked , ci )
}
2024-07-31 23:00:14 +09:00
}
2024-08-25 21:21:24 +03:00
}
2024-07-31 23:00:14 +09:00
}
2023-10-31 09:44:57 +00:00
}
} else {
2024-08-25 21:21:24 +03:00
chatItemView ( chatItem , range , prevItem , timeSeparation )
2024-07-31 23:00:14 +09:00
. overlay {
if let selected = selectedChatItems , chatItem . canBeDeletedForSelf {
Color . clear
. contentShape ( Rectangle ( ) )
. onTapGesture {
let checked = selected . contains ( chatItem . id )
selectUnselectChatItem ( select : ! checked , chatItem )
}
}
}
2024-07-03 10:24:26 +01:00
}
}
. onAppear {
2024-07-29 09:49:43 +01:00
if let range {
if let items = unreadItems ( range ) {
waitToMarkRead {
for ci in items {
await apiMarkChatItemRead ( chat . chatInfo , ci )
}
}
}
} else if chatItem . isRcvNew {
waitToMarkRead {
await apiMarkChatItemRead ( chat . chatInfo , chatItem )
}
}
2024-07-03 10:24:26 +01:00
}
}
2024-07-29 09:49:43 +01:00
private func unreadItems ( _ range : ClosedRange < Int > ) -> [ ChatItem ] ? {
2024-07-29 23:17:14 +03:00
let im = ItemsModel . shared
2024-07-29 09:49:43 +01:00
let items = range . compactMap { i in
2024-07-29 23:17:14 +03:00
if i >= 0 && i < im . reversedChatItems . count {
let ci = im . reversedChatItems [ i ]
2024-07-29 09:49:43 +01:00
return if ci . isRcvNew { ci } else { nil }
} else {
return nil
}
}
return if items . isEmpty { nil } else { items }
}
private func waitToMarkRead ( _ op : @ Sendable @ escaping ( ) async -> Void ) {
2024-07-03 10:24:26 +01:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.6 ) {
if m . chatId = = chat . chatInfo . id {
2024-07-29 09:49:43 +01:00
Task ( operation : op )
2023-10-31 09:44:57 +00:00
}
}
}
2024-08-25 21:21:24 +03:00
@ ViewBuilder func chatItemView ( _ ci : ChatItem , _ range : ClosedRange < Int > ? , _ prevItem : ChatItem ? , _ itemSeparation : ItemSeparation ) -> some View {
let bottomPadding : Double = itemSeparation . largeGap ? 10 : 2
2023-10-31 09:44:57 +00:00
if case let . groupRcv ( member ) = ci . chatDir ,
case let . group ( groupInfo ) = chat . chatInfo {
let ( prevMember , memCount ) : ( GroupMember ? , Int ) =
2024-07-31 23:00:14 +09:00
if let range = range {
m . getPrevHiddenMember ( member , range )
} else {
( nil , 1 )
}
2023-10-31 09:44:57 +00:00
if prevItem = = nil || showMemberImage ( member , prevItem ) || prevMember != nil {
2023-08-15 13:02:23 +01:00
VStack ( alignment : . leading , spacing : 4 ) {
if ci . content . showMemberName {
2024-07-15 08:35:37 +01:00
let t = if memCount = = 1 && member . memberRole > . member {
Text ( member . memberRole . text + " " ) . fontWeight ( . semibold ) + Text ( member . displayName )
} else {
Text ( memberNames ( member , prevMember , memCount ) )
}
t
2023-08-15 13:02:23 +01:00
. font ( . caption )
. foregroundStyle ( . secondary )
2024-05-03 15:34:15 +04:00
. lineLimit ( 2 )
2024-07-31 23:00:14 +09:00
. padding ( . leading , memberImageSize + 14 + ( selectedChatItems != nil && ci . canBeDeletedForSelf ? 12 + 24 : 0 ) )
2023-08-15 13:02:23 +01:00
. padding ( . top , 7 )
}
2024-07-31 23:00:14 +09:00
HStack ( alignment : . center , spacing : 0 ) {
if selectedChatItems != nil && ci . canBeDeletedForSelf {
SelectedChatItem ( ciId : ci . id , selectedChatItems : $ selectedChatItems )
. padding ( . trailing , 12 )
}
HStack ( alignment : . top , spacing : 8 ) {
2024-08-04 22:24:08 +01:00
MemberProfileImage ( member , size : memberImageSize , backgroundColor : theme . colors . background )
2024-07-31 23:00:14 +09:00
. onTapGesture {
2024-08-13 21:37:48 +03:00
if let member = m . getGroupMember ( member . groupMemberId ) {
selectedMember = member
2024-07-31 23:00:14 +09:00
} else {
Task {
await m . loadGroupMembers ( groupInfo ) {
selectedMember = m . getGroupMember ( member . groupMemberId )
}
2023-10-31 09:44:57 +00:00
}
}
}
2024-08-25 21:21:24 +03:00
chatItemWithMenu ( ci , range , maxWidth , itemSeparation )
2024-07-31 23:00:14 +09:00
}
2023-08-15 13:02:23 +01:00
}
2023-08-14 17:34:22 +04:00
}
2024-08-25 21:21:24 +03:00
. padding ( . bottom , bottomPadding )
2023-08-15 13:02:23 +01:00
. padding ( . trailing )
. padding ( . leading , 12 )
} else {
2024-07-31 23:00:14 +09:00
HStack ( alignment : . center , spacing : 0 ) {
if selectedChatItems != nil && ci . canBeDeletedForSelf {
SelectedChatItem ( ciId : ci . id , selectedChatItems : $ selectedChatItems )
. padding ( . leading , 12 )
}
2024-08-25 21:21:24 +03:00
chatItemWithMenu ( ci , range , maxWidth , itemSeparation )
2024-07-31 23:00:14 +09:00
. padding ( . trailing )
. padding ( . leading , memberImageSize + 8 + 12 )
}
2024-08-25 21:21:24 +03:00
. padding ( . bottom , bottomPadding )
2022-08-15 21:07:11 +01:00
}
2023-10-31 09:44:57 +00:00
} else {
2024-07-31 23:00:14 +09:00
HStack ( alignment : . center , spacing : 0 ) {
if selectedChatItems != nil && ci . canBeDeletedForSelf {
if chat . chatInfo . chatType = = . group {
SelectedChatItem ( ciId : ci . id , selectedChatItems : $ selectedChatItems )
. padding ( . leading , 12 )
} else {
SelectedChatItem ( ciId : ci . id , selectedChatItems : $ selectedChatItems )
. padding ( . leading )
}
}
2024-08-25 21:21:24 +03:00
chatItemWithMenu ( ci , range , maxWidth , itemSeparation )
2024-07-31 23:00:14 +09:00
. padding ( . horizontal )
}
2024-08-25 21:21:24 +03:00
. padding ( . bottom , bottomPadding )
2022-08-15 21:07:11 +01:00
}
}
2023-08-15 13:02:23 +01:00
2023-10-31 09:44:57 +00:00
private func memberNames ( _ member : GroupMember , _ prevMember : GroupMember ? , _ memCount : Int ) -> LocalizedStringKey {
let name = member . displayName
return if let prevName = prevMember ? . displayName {
memCount > 2
? " \( name ) , \( prevName ) and \( memCount - 2 ) members "
: " \( name ) and \( prevName ) "
} else {
" \( name ) "
}
}
2023-05-14 20:07:34 +03:00
2024-08-25 21:21:24 +03:00
@ ViewBuilder func chatItemWithMenu ( _ ci : ChatItem , _ range : ClosedRange < Int > ? , _ maxWidth : CGFloat , _ itemSeparation : ItemSeparation ) -> some View {
2022-12-03 15:40:31 +04:00
let alignment : Alignment = ci . chatDir . sent ? . trailing : . leading
2023-05-16 10:34:25 +02:00
VStack ( alignment : alignment . horizontal , spacing : 3 ) {
2023-10-31 09:44:57 +00:00
ChatItemView (
chat : chat ,
chatItem : ci ,
maxWidth : maxWidth ,
2024-07-03 10:24:26 +01:00
revealed : . constant ( revealed ) ,
2024-07-24 00:11:42 +07:00
allowMenu : $ allowMenu
2023-10-31 09:44:57 +00:00
)
2024-08-25 21:21:24 +03:00
. environment ( \ . showTimestamp , itemSeparation . timestamp )
2024-07-04 19:37:03 +02:00
. modifier ( ChatItemClipped ( ci ) )
2024-07-03 10:24:26 +01:00
. contextMenu { menu ( ci , range , live : composeState . liveMessage != nil ) }
2023-10-31 09:44:57 +00:00
. accessibilityLabel ( " " )
2023-05-19 18:50:48 +02:00
if ci . content . msgContent != nil && ( ci . meta . itemDeleted = = nil || revealed ) && ci . reactions . count > 0 {
2023-10-31 09:44:57 +00:00
chatItemReactions ( ci )
2023-05-15 12:28:53 +02:00
. padding ( . bottom , 4 )
}
}
2022-12-03 15:40:31 +04:00
. confirmationDialog ( " Delete message? " , isPresented : $ showDeleteMessage , titleVisibility : . visible ) {
Button ( " Delete for me " , role : . destructive ) {
2024-07-31 23:00:14 +09:00
deleteMessage ( . cidmInternal , moderate : false )
2022-08-15 21:07:11 +01:00
}
2024-04-12 13:10:47 +04:00
if let di = deletingItem , di . meta . deletable && ! di . localNote {
2024-07-31 23:00:14 +09:00
Button ( broadcastDeleteButtonText ( chat ) , role : . destructive ) {
deleteMessage ( . cidmBroadcast , moderate : false )
2022-12-03 15:40:31 +04:00
}
2022-03-30 08:57:42 +01:00
}
2022-03-30 20:37:47 +04:00
}
2023-10-31 09:44:57 +00:00
. confirmationDialog ( deleteMessagesTitle , isPresented : $ showDeleteMessages , titleVisibility : . visible ) {
Button ( " Delete for me " , role : . destructive ) {
2024-07-31 23:00:14 +09:00
deleteMessages ( chat , deletingItems , moderate : false )
2023-10-31 09:44:57 +00:00
}
}
2022-12-03 15:40:31 +04:00
. frame ( maxWidth : maxWidth , maxHeight : . infinity , alignment : alignment )
. frame ( minWidth : 0 , maxWidth : . infinity , alignment : alignment )
2023-05-09 20:43:21 +04:00
. sheet ( isPresented : $ showChatItemInfoSheet , onDismiss : {
chatItemInfo = nil
} ) {
2023-05-19 18:50:48 +02:00
ChatItemInfoView ( ci : ci , chatItemInfo : $ chatItemInfo )
2023-05-09 20:43:21 +04:00
}
2024-04-16 12:28:39 +04:00
. sheet ( isPresented : $ showForwardingSheet ) {
if #available ( iOS 16.0 , * ) {
ChatItemForwardingView ( ci : ci , fromChatInfo : chat . chatInfo , composeState : $ composeState )
. presentationDetents ( [ . fraction ( 0.8 ) ] )
} else {
ChatItemForwardingView ( ci : ci , fromChatInfo : chat . chatInfo , composeState : $ composeState )
}
}
2022-12-03 15:40:31 +04:00
}
2023-05-15 12:28:53 +02:00
2023-10-31 09:44:57 +00:00
private func showMemberImage ( _ member : GroupMember , _ prevItem : ChatItem ? ) -> Bool {
switch ( prevItem ? . chatDir ) {
case . groupSnd : return true
case let . groupRcv ( prevMember ) : return prevMember . groupMemberId != member . groupMemberId
default : return false
}
}
private func chatItemReactions ( _ ci : ChatItem ) -> some View {
2023-05-15 12:28:53 +02:00
HStack ( spacing : 4 ) {
2023-05-16 10:34:25 +02:00
ForEach ( ci . reactions , id : \ . reaction ) { r in
let v = HStack ( spacing : 4 ) {
2023-05-15 12:28:53 +02:00
switch r . reaction {
2023-05-16 10:34:25 +02:00
case let . emoji ( emoji ) : Text ( emoji . rawValue ) . font ( . caption )
case . unknown : EmptyView ( )
2023-05-15 12:28:53 +02:00
}
if r . totalReacted > 1 {
2023-05-16 10:34:25 +02:00
Text ( " \( r . totalReacted ) " )
. font ( . caption )
. fontWeight ( r . userReacted ? . bold : . light )
2024-07-03 22:42:13 +01:00
. foregroundColor ( r . userReacted ? theme . colors . primary : theme . colors . secondary )
2023-05-15 12:28:53 +02:00
}
}
2023-05-16 10:34:25 +02:00
. padding ( . horizontal , 6 )
2023-05-15 12:28:53 +02:00
. padding ( . vertical , 4 )
2023-05-16 10:34:25 +02:00
if chat . chatInfo . featureEnabled ( . reactions ) && ( ci . allowAddReaction || r . userReacted ) {
v . onTapGesture {
2023-10-31 09:44:57 +00:00
setReaction ( ci , add : ! r . userReacted , reaction : r . reaction )
2023-05-16 10:34:25 +02:00
}
} else {
v
}
2023-05-15 12:28:53 +02:00
}
}
}
2024-07-03 10:24:26 +01:00
@ ViewBuilder
private func menu ( _ ci : ChatItem , _ range : ClosedRange < Int > ? , live : Bool ) -> some View {
2023-02-09 15:10:35 +04:00
if let mc = ci . content . msgContent , ci . meta . itemDeleted = = nil || revealed {
2023-05-18 11:43:44 +02:00
if chat . chatInfo . featureEnabled ( . reactions ) && ci . allowAddReaction ,
2024-07-03 10:24:26 +01:00
availableReactions . count > 0 {
reactionsGroup
2023-05-16 10:34:25 +02:00
}
2024-01-18 22:57:14 +07:00
if ci . meta . itemDeleted = = nil && ! ci . isLiveDummy && ! live && ! ci . localNote {
2024-07-03 10:24:26 +01:00
replyButton
2022-11-24 21:18:28 +04:00
}
2023-12-24 07:15:31 +08:00
let fileSource = getLoadedFileSource ( ci . file )
let fileExists = if let fs = fileSource , FileManager . default . fileExists ( atPath : getAppFilePath ( fs . filePath ) . path ) { true } else { false }
let copyAndShareAllowed = ! ci . content . text . isEmpty || ( ci . content . msgContent ? . isImage = = true && fileExists )
if copyAndShareAllowed {
2024-07-03 10:24:26 +01:00
shareButton ( ci )
copyButton ( ci )
2023-12-24 07:15:31 +08:00
}
if let fileSource = fileSource , fileExists {
2022-12-24 00:22:12 +03:00
if case . image = ci . content . msgContent , let image = getLoadedImage ( ci . file ) {
2022-12-26 18:08:58 +04:00
if image . imageData != nil {
2024-07-03 10:24:26 +01:00
saveButton ( file : fileSource )
2022-12-24 00:22:12 +03:00
} else {
2024-07-03 10:24:26 +01:00
saveButton ( image : image )
2022-12-24 00:22:12 +03:00
}
2022-12-26 18:08:58 +04:00
} else {
2024-07-03 10:24:26 +01:00
saveButton ( file : fileSource )
2022-12-03 15:40:31 +04:00
}
2024-04-19 20:21:35 +04:00
} else if let file = ci . file , case . rcvInvitation = file . fileStatus , fileSizeValid ( file ) {
2024-07-03 10:24:26 +01:00
downloadButton ( file : file )
2022-12-26 18:08:58 +04:00
}
2023-01-11 13:29:09 +00:00
if ci . meta . editable && ! mc . isVoice && ! live {
2024-07-03 10:24:26 +01:00
editButton ( chatItem )
2022-08-15 21:07:11 +01:00
}
2024-04-19 20:21:35 +04:00
if ci . meta . itemDeleted = = nil
&& ( ci . file = = nil || ( fileSource != nil && fileExists ) )
&& ! ci . isLiveDummy && ! live {
2024-07-03 10:24:26 +01:00
forwardButton
2024-04-16 12:28:39 +04:00
}
2024-01-26 00:48:40 +07:00
if ! ci . isLiveDummy {
2024-07-03 10:24:26 +01:00
viewInfoButton ( ci )
2024-01-26 00:48:40 +07:00
}
2022-12-03 19:21:47 +00:00
if revealed {
2024-07-03 10:24:26 +01:00
hideButton ( )
2022-12-03 19:21:47 +00:00
}
2024-01-18 22:57:14 +07:00
if ci . meta . itemDeleted = = nil && ! ci . localNote ,
2023-03-30 14:10:13 +04:00
let file = ci . file ,
2024-01-18 22:57:14 +07:00
let cancelAction = file . cancelAction {
2024-07-03 10:24:26 +01:00
cancelFileButton ( file . fileId , cancelAction )
2023-03-30 14:10:13 +04:00
}
2023-01-11 13:29:09 +00:00
if ! live || ! ci . meta . isLive {
2024-07-03 10:24:26 +01:00
deleteButton ( ci )
2023-01-11 13:29:09 +00:00
}
2024-07-31 23:00:14 +09:00
if let ( groupInfo , _ ) = ci . memberToModerate ( chat . chatInfo ) , ci . chatDir != . groupSnd {
2024-07-03 10:24:26 +01:00
moderateButton ( ci , groupInfo )
2023-03-06 21:57:58 +00:00
}
2023-02-09 15:10:35 +04:00
} else if ci . meta . itemDeleted != nil {
2023-10-31 09:44:57 +00:00
if revealed {
2024-07-03 10:24:26 +01:00
hideButton ( )
2023-10-31 09:44:57 +00:00
} else if ! ci . isDeletedContent {
2024-07-03 10:24:26 +01:00
revealButton ( ci )
2023-10-31 09:44:57 +00:00
} else if range != nil {
2024-07-03 10:24:26 +01:00
expandButton ( )
2023-03-06 21:57:58 +00:00
}
2024-07-03 10:24:26 +01:00
viewInfoButton ( ci )
deleteButton ( ci )
2022-12-03 15:40:31 +04:00
} else if ci . isDeletedContent {
2024-07-03 10:24:26 +01:00
viewInfoButton ( ci )
deleteButton ( ci )
2023-11-11 00:09:01 +08:00
} else if ci . mergeCategory != nil && ( ( range ? . count ? ? 0 ) > 1 || revealed ) {
2024-07-03 10:24:26 +01:00
if revealed { shrinkButton ( ) } else { expandButton ( ) }
deleteButton ( ci )
2024-04-25 13:20:52 +04:00
} else if ci . showLocalDelete {
2024-07-03 10:24:26 +01:00
deleteButton ( ci )
} else {
EmptyView ( )
2022-12-03 15:40:31 +04:00
}
2024-07-31 23:00:14 +09:00
if selectedChatItems = = nil && ci . canBeDeletedForSelf {
Divider ( )
selectButton ( ci )
}
2022-12-03 15:40:31 +04:00
}
2024-07-03 10:24:26 +01:00
var replyButton : Button < some View > {
Button {
2022-12-03 15:53:46 +04:00
withAnimation {
if composeState . editing {
2024-07-03 10:24:26 +01:00
composeState = ComposeState ( contextItem : . quotedItem ( chatItem : chatItem ) )
2022-12-03 15:53:46 +04:00
} else {
2024-07-03 10:24:26 +01:00
composeState = composeState . copy ( contextItem : . quotedItem ( chatItem : chatItem ) )
2022-12-03 15:53:46 +04:00
}
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Reply " , comment : " chat item action " ) ,
systemImage : " arrowshape.turn.up.left "
)
2022-12-03 15:53:46 +04:00
}
}
2023-05-16 10:34:25 +02:00
2024-07-03 10:24:26 +01:00
var forwardButton : Button < some View > {
Button {
2024-04-16 12:28:39 +04:00
showForwardingSheet = true
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Forward " , comment : " chat item action " ) ,
systemImage : " arrowshape.turn.up.forward "
)
2024-04-16 12:28:39 +04:00
}
}
2024-07-03 10:24:26 +01:00
private var reactionsGroup : some View {
if #available ( iOS 16.4 , * ) {
return ControlGroup {
if availableReactions . count > 4 {
reactions ( till : 3 )
Menu {
reactions ( from : 3 )
} label : {
Image ( systemName : " ellipsis " )
}
} else { reactions ( ) }
} . controlGroupStyle ( . compactMenu )
} else {
return Menu {
reactions ( )
} label : {
Label (
NSLocalizedString ( " React… " , comment : " chat item menu " ) ,
systemImage : " face.smiling "
)
}
}
2023-06-13 21:24:28 +03:00
}
2024-07-03 10:24:26 +01:00
func reactions ( from : Int ? = nil , till : Int ? = nil ) -> some View {
2024-08-04 20:48:54 +01:00
ForEach ( availableReactions [ ( from ? ? 0 ) . . < ( till ? ? availableReactions . count ) ] ) { reaction in
2024-07-03 10:24:26 +01:00
Button ( reaction . text ) {
setReaction ( chatItem , add : true , reaction : reaction )
}
2023-05-16 10:34:25 +02:00
}
2023-06-13 21:24:28 +03:00
}
2024-07-03 10:24:26 +01:00
// / R e a c t i o n s , w h i c h h a s n o t b e e n u s e d y e t
private var availableReactions : Array < MsgReaction > {
MsgReaction . values
. filter { reaction in
! chatItem . reactions . contains {
$0 . userReacted && $0 . reaction = = reaction
}
}
2023-05-16 10:34:25 +02:00
}
2023-10-31 09:44:57 +00:00
private func setReaction ( _ ci : ChatItem , add : Bool , reaction : MsgReaction ) {
2023-05-16 10:34:25 +02:00
Task {
do {
2023-05-19 18:50:48 +02:00
let cInfo = chat . chatInfo
2023-05-16 10:34:25 +02:00
let chatItem = try await apiChatItemReaction (
2023-05-19 18:50:48 +02:00
type : cInfo . chatType ,
id : cInfo . apiId ,
2023-05-16 10:34:25 +02:00
itemId : ci . id ,
2023-05-17 10:31:27 +02:00
add : add ,
reaction : reaction
2023-05-16 10:34:25 +02:00
)
await MainActor . run {
2023-10-31 09:44:57 +00:00
m . updateChatItem ( chat . chatInfo , chatItem )
2023-05-16 10:34:25 +02:00
}
} catch let error {
logger . error ( " apiChatItemReaction error: \( responseError ( error ) ) " )
}
}
}
2024-07-03 10:24:26 +01:00
private func shareButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
2022-12-03 15:53:46 +04:00
var shareItems : [ Any ] = [ ci . content . text ]
if case . image = ci . content . msgContent , let image = getLoadedImage ( ci . file ) {
shareItems . append ( image )
}
showShareSheet ( items : shareItems )
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Share " , comment : " chat item action " ) ,
systemImage : " square.and.arrow.up "
)
2022-12-03 15:53:46 +04:00
}
}
2024-07-03 10:24:26 +01:00
private func copyButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
2022-12-03 15:53:46 +04:00
if case let . image ( text , _ ) = ci . content . msgContent ,
text = = " " ,
let image = getLoadedImage ( ci . file ) {
UIPasteboard . general . image = image
} else {
UIPasteboard . general . string = ci . content . text
}
2024-07-03 10:24:26 +01:00
} label : {
Label ( " Copy " , systemImage : " doc.on.doc " )
2022-12-03 15:53:46 +04:00
}
}
2024-07-03 10:24:26 +01:00
func saveButton ( image : UIImage ) -> Button < some View > {
Button {
2022-12-03 15:53:46 +04:00
UIImageWriteToSavedPhotosAlbum ( image , nil , nil , nil )
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Save " , comment : " chat item action " ) ,
systemImage : " square.and.arrow.down "
)
2022-12-03 15:53:46 +04:00
}
}
2024-07-03 10:24:26 +01:00
func saveButton ( file : CryptoFile ) -> Button < some View > {
Button {
saveCryptoFile ( file )
} label : {
Label (
NSLocalizedString ( " Save " , comment : " chat item action " ) ,
systemImage : file . cryptoArgs = = nil ? " square.and.arrow.down " : " lock.open "
)
2022-12-03 15:53:46 +04:00
}
}
2024-04-19 20:21:35 +04:00
2024-07-03 10:24:26 +01:00
func downloadButton ( file : CIFile ) -> Button < some View > {
Button {
2024-04-19 20:21:35 +04:00
Task {
logger . debug ( " ChatView downloadFileAction, in Task " )
if let user = m . currentUser {
await receiveFile ( user : user , fileId : file . fileId )
}
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Download " , comment : " chat item action " ) ,
systemImage : " arrow.down.doc "
)
2024-04-19 20:21:35 +04:00
}
}
2024-07-03 10:24:26 +01:00
private func editButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
2022-12-03 15:53:46 +04:00
withAnimation {
composeState = ComposeState ( editingItem : ci )
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Edit " , comment : " chat item action " ) ,
systemImage : " square.and.pencil "
)
2022-12-03 15:53:46 +04:00
}
}
2022-12-03 19:21:47 +00:00
2024-07-31 23:00:14 +09:00
private func selectButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.5 ) {
withAnimation {
selectUnselectChatItem ( select : true , ci )
}
}
} label : {
Label (
NSLocalizedString ( " Select " , comment : " chat item action " ) ,
systemImage : " checkmark.circle "
)
}
}
2024-07-03 10:24:26 +01:00
private func viewInfoButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
2023-05-09 20:43:21 +04:00
Task {
do {
2023-05-19 18:50:48 +02:00
let cInfo = chat . chatInfo
let ciInfo = try await apiGetChatItemInfo ( type : cInfo . chatType , id : cInfo . apiId , itemId : ci . id )
2023-05-09 20:43:21 +04:00
await MainActor . run {
chatItemInfo = ciInfo
}
2023-07-28 13:16:52 +04:00
if case let . group ( gInfo ) = chat . chatInfo {
2024-07-11 10:57:56 +03:00
await m . loadGroupMembers ( gInfo )
2023-07-28 13:16:52 +04:00
}
2023-05-09 20:43:21 +04:00
} catch let error {
logger . error ( " apiGetChatItemInfo error: \( responseError ( error ) ) " )
}
await MainActor . run { showChatItemInfoSheet = true }
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Info " , comment : " chat item action " ) ,
systemImage : " info.circle "
)
2023-05-09 20:43:21 +04:00
}
}
2024-07-03 10:24:26 +01:00
private func cancelFileButton ( _ fileId : Int64 , _ cancelAction : CancelAction ) -> Button < some View > {
Button {
2023-03-30 14:10:13 +04:00
AlertManager . shared . showAlert ( Alert (
2023-04-18 12:48:36 +04:00
title : Text ( cancelAction . alert . title ) ,
message : Text ( cancelAction . alert . message ) ,
primaryButton : . destructive ( Text ( cancelAction . alert . confirm ) ) {
2023-03-30 14:10:13 +04:00
Task {
2023-10-31 09:44:57 +00:00
if let user = m . currentUser {
2023-03-30 14:10:13 +04:00
await cancelFile ( user : user , fileId : fileId )
}
}
} ,
secondaryButton : . cancel ( )
) )
2024-07-03 10:24:26 +01:00
} label : {
Label (
cancelAction . uiAction ,
systemImage : " xmark "
)
2023-03-30 14:10:13 +04:00
}
}
2024-07-03 10:24:26 +01:00
private func hideButton ( ) -> Button < some View > {
Button {
withConditionalAnimation {
revealedChatItem = nil
2024-03-21 23:12:47 +04:00
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Hide " , comment : " chat item action " ) ,
systemImage : " eye.slash "
)
2022-12-03 19:21:47 +00:00
}
}
2024-07-03 10:24:26 +01:00
private func deleteButton ( _ ci : ChatItem ) -> Button < some View > {
Button ( role : . destructive ) {
2024-04-25 13:20:52 +04:00
if ! revealed ,
2023-10-31 09:44:57 +00:00
let currIndex = m . getChatItemIndex ( ci ) ,
let ciCategory = ci . mergeCategory {
let ( prevHidden , _ ) = m . getPrevShownChatItem ( currIndex , ciCategory )
if let range = itemsRange ( currIndex , prevHidden ) {
var itemIds : [ Int64 ] = [ ]
for i in range {
2024-07-29 23:17:14 +03:00
itemIds . append ( ItemsModel . shared . reversedChatItems [ i ] . id )
2023-10-31 09:44:57 +00:00
}
showDeleteMessages = true
deletingItems = itemIds
} else {
showDeleteMessage = true
deletingItem = ci
}
} else {
showDeleteMessage = true
deletingItem = ci
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Delete " , comment : " chat item action " ) ,
systemImage : " trash "
)
2022-12-03 15:40:31 +04:00
}
2022-08-15 21:07:11 +01:00
}
2023-03-06 21:57:58 +00:00
2023-10-31 09:44:57 +00:00
private func itemsRange ( _ currIndex : Int ? , _ prevHidden : Int ? ) -> ClosedRange < Int > ? {
if let currIndex = currIndex ,
let prevHidden = prevHidden ,
prevHidden > currIndex {
currIndex . . . prevHidden
} else {
nil
}
}
2024-07-03 10:24:26 +01:00
private func moderateButton ( _ ci : ChatItem , _ groupInfo : GroupInfo ) -> Button < some View > {
Button ( role : . destructive ) {
2023-03-06 21:57:58 +00:00
AlertManager . shared . showAlert ( Alert (
title : Text ( " Delete member message? " ) ,
message : Text (
groupInfo . fullGroupPreferences . fullDelete . on
? " The message will be deleted for all members. "
: " The message will be marked as moderated for all members. "
) ,
primaryButton : . destructive ( Text ( " Delete " ) ) {
deletingItem = ci
2024-07-31 23:00:14 +09:00
deleteMessage ( . cidmBroadcast , moderate : true )
2023-03-06 21:57:58 +00:00
} ,
secondaryButton : . cancel ( )
) )
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Moderate " , comment : " chat item action " ) ,
systemImage : " flag "
)
2023-03-06 21:57:58 +00:00
}
}
2024-07-03 10:24:26 +01:00
private func revealButton ( _ ci : ChatItem ) -> Button < some View > {
Button {
withConditionalAnimation {
revealedChatItem = ci
2024-03-21 23:12:47 +04:00
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Reveal " , comment : " chat item action " ) ,
systemImage : " eye "
)
2022-03-30 08:57:42 +01:00
}
2022-12-03 15:40:31 +04:00
}
2023-10-31 09:44:57 +00:00
2024-07-03 10:24:26 +01:00
private func expandButton ( ) -> Button < some View > {
Button {
withConditionalAnimation {
revealedChatItem = chatItem
2023-10-31 09:44:57 +00:00
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Expand " , comment : " chat item action " ) ,
systemImage : " arrow.up.and.line.horizontal.and.arrow.down "
)
2023-10-31 09:44:57 +00:00
}
}
2024-07-03 10:24:26 +01:00
private func shrinkButton ( ) -> Button < some View > {
Button {
withConditionalAnimation {
revealedChatItem = nil
2023-10-31 09:44:57 +00:00
}
2024-07-03 10:24:26 +01:00
} label : {
Label (
NSLocalizedString ( " Hide " , comment : " chat item action " ) ,
systemImage : " arrow.down.and.line.horizontal.and.arrow.up "
)
2023-10-31 09:44:57 +00:00
}
}
var deleteMessagesTitle : LocalizedStringKey {
let n = deletingItems . count
return n = = 1 ? " Delete message? " : " Delete \( n ) messages? "
}
2024-07-31 23:00:14 +09:00
private func selectUnselectChatItem ( select : Bool , _ ci : ChatItem ) {
selectedChatItems = selectedChatItems ? ? [ ]
var itemIds : [ Int64 ] = [ ]
if ! revealed ,
let currIndex = m . getChatItemIndex ( ci ) ,
let ciCategory = ci . mergeCategory {
let ( prevHidden , _ ) = m . getPrevShownChatItem ( currIndex , ciCategory )
if let range = itemsRange ( currIndex , prevHidden ) {
for i in range {
itemIds . append ( ItemsModel . shared . reversedChatItems [ i ] . id )
2023-10-31 09:44:57 +00:00
}
2024-07-31 23:00:14 +09:00
} else {
itemIds . append ( ci . id )
}
} else {
itemIds . append ( ci . id )
}
if select {
if let sel = selectedChatItems {
selectedChatItems = sel . union ( itemIds )
2023-10-31 09:44:57 +00:00
}
2024-07-31 23:00:14 +09:00
} else {
itemIds . forEach { selectedChatItems ? . remove ( $0 ) }
2023-10-31 09:44:57 +00:00
}
}
2024-07-31 23:00:14 +09:00
private func deleteMessage ( _ mode : CIDeleteMode , moderate : Bool ) {
2023-10-31 09:44:57 +00:00
logger . debug ( " ChatView deleteMessage " )
Task {
logger . debug ( " ChatView deleteMessage: in Task " )
do {
if let di = deletingItem {
2024-07-31 07:56:56 +09:00
let r = if case . cidmBroadcast = mode ,
2024-07-31 23:00:14 +09:00
moderate ,
let ( groupInfo , _ ) = di . memberToModerate ( chat . chatInfo ) {
2024-07-31 07:56:56 +09:00
try await apiDeleteMemberChatItems (
2023-10-31 09:44:57 +00:00
groupId : groupInfo . apiId ,
2024-07-31 07:56:56 +09:00
itemIds : [ di . id ]
2023-10-31 09:44:57 +00:00
)
} else {
2024-07-31 07:56:56 +09:00
try await apiDeleteChatItems (
2023-10-31 09:44:57 +00:00
type : chat . chatInfo . chatType ,
id : chat . chatInfo . apiId ,
2024-07-31 07:56:56 +09:00
itemIds : [ di . id ] ,
2023-10-31 09:44:57 +00:00
mode : mode
)
}
2024-07-31 07:56:56 +09:00
if let itemDeletion = r . first {
await MainActor . run {
deletingItem = nil
if let toItem = itemDeletion . toChatItem {
_ = m . upsertChatItem ( chat . chatInfo , toItem . chatItem )
} else {
m . removeChatItem ( chat . chatInfo , itemDeletion . deletedChatItem . chatItem )
}
2023-10-31 09:44:57 +00:00
}
}
}
} catch {
logger . error ( " ChatView.deleteMessage error: \( error . localizedDescription ) " )
}
}
2024-07-31 23:00:14 +09:00
}
private struct SelectedChatItem : View {
@ EnvironmentObject var theme : AppTheme
var ciId : Int64
@ Binding var selectedChatItems : Set < Int64 > ?
@ State var checked : Bool = false
var body : some View {
Image ( systemName : checked ? " checkmark.circle.fill " : " circle " )
. resizable ( )
. foregroundColor ( checked ? theme . colors . primary : Color ( uiColor : . tertiaryLabel ) )
. frame ( width : 24 , height : 24 )
. onAppear {
checked = selectedChatItems ? . contains ( ciId ) = = true
}
. onChange ( of : selectedChatItems ) { selected in
checked = selected ? . contains ( ciId ) = = true
}
}
}
}
}
private func broadcastDeleteButtonText ( _ chat : Chat ) -> LocalizedStringKey {
chat . chatInfo . featureEnabled ( . fullDelete ) ? " Delete for everyone " : " Mark deleted for everyone "
}
private func deleteMessages ( _ chat : Chat , _ deletingItems : [ Int64 ] , _ mode : CIDeleteMode = . cidmInternal , moderate : Bool , _ onSuccess : @ escaping ( ) async -> Void = { } ) {
let itemIds = deletingItems
if itemIds . count > 0 {
let chatInfo = chat . chatInfo
Task {
do {
let deletedItems = if case . cidmBroadcast = mode ,
moderate ,
case . group = chat . chatInfo {
try await apiDeleteMemberChatItems (
groupId : chatInfo . apiId ,
itemIds : itemIds
)
} else {
try await apiDeleteChatItems (
type : chatInfo . chatType ,
id : chatInfo . apiId ,
itemIds : itemIds ,
mode : mode
)
}
await MainActor . run {
for di in deletedItems {
if let toItem = di . toChatItem {
_ = ChatModel . shared . upsertChatItem ( chat . chatInfo , toItem . chatItem )
} else {
ChatModel . shared . removeChatItem ( chatInfo , di . deletedChatItem . chatItem )
}
}
}
await onSuccess ( )
} catch {
logger . error ( " ChatView.deleteMessages error: \( error . localizedDescription ) " )
}
2022-03-30 08:57:42 +01:00
}
}
2022-01-29 11:10:04 +00:00
}
2024-07-03 22:42:13 +01:00
private func buildTheme ( ) -> AppTheme {
if let cId = ChatModel . shared . chatId , let chat = ChatModel . shared . getChat ( cId ) {
let perChatTheme = if case let . direct ( contact ) = chat . chatInfo {
contact . uiThemes ? . preferredMode ( ! AppTheme . shared . colors . isLight )
} else if case let . group ( groupInfo ) = chat . chatInfo {
groupInfo . uiThemes ? . preferredMode ( ! AppTheme . shared . colors . isLight )
} else {
nil as ThemeModeOverride ?
}
let overrides = if perChatTheme != nil {
ThemeManager . currentColors ( nil , perChatTheme , ChatModel . shared . currentUser ? . uiThemes , themeOverridesDefault . get ( ) )
} else {
nil as ThemeManager . ActiveTheme ?
}
let theme = overrides ? ? CurrentColors
return AppTheme ( name : theme . name , base : theme . base , colors : theme . colors , appColors : theme . appColors , wallpaper : theme . wallpaper )
} else {
return AppTheme . shared
}
}
2024-04-19 23:20:31 +07:00
struct ToggleNtfsButton : View {
@ ObservedObject var chat : Chat
var body : some View {
Button {
toggleNotifications ( chat , enableNtfs : ! chat . chatInfo . ntfsEnabled )
} label : {
if chat . chatInfo . ntfsEnabled {
Label ( " Mute " , systemImage : " speaker.slash " )
} else {
Label ( " Unmute " , systemImage : " speaker.wave.2 " )
}
2022-08-20 12:47:48 +01:00
}
}
}
func toggleNotifications ( _ chat : Chat , enableNtfs : Bool ) {
2023-06-19 11:13:30 +01:00
var chatSettings = chat . chatInfo . chatSettings ? ? ChatSettings . defaults
2023-10-11 23:07:05 +01:00
chatSettings . enableNtfs = enableNtfs ? . all : . none
2023-06-19 11:13:30 +01:00
updateChatSettings ( chat , chatSettings : chatSettings )
}
func toggleChatFavorite ( _ chat : Chat , favorite : Bool ) {
var chatSettings = chat . chatInfo . chatSettings ? ? ChatSettings . defaults
chatSettings . favorite = favorite
updateChatSettings ( chat , chatSettings : chatSettings )
}
func updateChatSettings ( _ chat : Chat , chatSettings : ChatSettings ) {
2022-08-20 12:47:48 +01:00
Task {
do {
try await apiSetChatSettings ( type : chat . chatInfo . chatType , id : chat . chatInfo . apiId , chatSettings : chatSettings )
await MainActor . run {
switch chat . chatInfo {
case var . direct ( contact ) :
contact . chatSettings = chatSettings
ChatModel . shared . updateContact ( contact )
case var . group ( groupInfo ) :
groupInfo . chatSettings = chatSettings
ChatModel . shared . updateGroup ( groupInfo )
default : ( )
}
}
} catch let error {
logger . error ( " apiSetChatSettings error \( responseError ( error ) ) " )
}
}
}
2022-01-29 23:37:02 +00:00
struct ChatView_Previews : PreviewProvider {
static var previews : some View {
let chatModel = ChatModel ( )
2022-02-02 12:51:39 +00:00
chatModel . chatId = " @1 "
2024-07-29 23:17:14 +03:00
ItemsModel . shared . reversedChatItems = [
2022-02-08 09:19:25 +00:00
ChatItem . getSample ( 1 , . directSnd , . now , " hello " ) ,
ChatItem . getSample ( 2 , . directRcv , . now , " hi " ) ,
ChatItem . getSample ( 3 , . directRcv , . now , " hi there " ) ,
2022-03-30 20:37:47 +04:00
ChatItem . getDeletedContentSample ( 4 ) ,
ChatItem . getSample ( 5 , . directRcv , . now , " hello again " ) ,
ChatItem . getSample ( 6 , . directSnd , . now , " hi there!!! " ) ,
ChatItem . getSample ( 7 , . directSnd , . now , " how are you? " ) ,
ChatItem . getSample ( 8 , . directSnd , . now , " 👍👍👍👍 " ) ,
ChatItem . getSample ( 9 , . directSnd , . now , " Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. " )
2022-01-29 23:37:02 +00:00
]
2022-06-03 13:19:41 +04:00
@ State var showChatInfo = false
2022-07-26 10:55:58 +04:00
return ChatView ( chat : Chat ( chatInfo : ChatInfo . sampleData . direct , chatItems : [ ] ) )
2022-01-29 23:37:02 +00:00
. environmentObject ( chatModel )
}
}