mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
ios: go to forwarded item or search result (#5679)
* ios: go to forwarded item or search result * react on touch * changes
This commit is contained in:
parent
b482d4d812
commit
fefddb3b5a
11 changed files with 228 additions and 64 deletions
|
@ -97,6 +97,22 @@ class ItemsModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadOpenChatNoWait(_ chatId: ChatId, _ openAroundItemId: ChatItem.ID? = nil) {
|
||||
navigationTimeoutTask?.cancel()
|
||||
loadChatTask?.cancel()
|
||||
loadChatTask = Task {
|
||||
// try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
||||
if !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
if openAroundItemId == nil {
|
||||
ChatModel.shared.chatId = chatId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatTagsModel: ObservableObject {
|
||||
|
@ -259,6 +275,7 @@ final class ChatModel: ObservableObject {
|
|||
@Published var deletedChats: Set<String> = []
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var openAroundItemId: ChatItem.ID? = nil
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GMember] = []
|
||||
|
|
|
@ -338,7 +338,7 @@ func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async {
|
|||
await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems)
|
||||
}
|
||||
|
||||
func loadChat(chatId: ChatId, search: String = "", clearItems: Bool = true) async {
|
||||
func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
let m = ChatModel.shared
|
||||
let im = ItemsModel.shared
|
||||
m.chatItemStatuses = [:]
|
||||
|
@ -348,7 +348,7 @@ func loadChat(chatId: ChatId, search: String = "", clearItems: Bool = true) asyn
|
|||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
}
|
||||
}
|
||||
await apiLoadMessages(chatId, search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage), im.chatState, search, { 0...0 })
|
||||
await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 })
|
||||
}
|
||||
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo {
|
||||
|
|
|
@ -16,6 +16,7 @@ func apiLoadMessages(
|
|||
_ pagination: ChatPagination,
|
||||
_ chatState: ActiveChatState,
|
||||
_ search: String = "",
|
||||
_ openAroundItemId: ChatItem.ID? = nil,
|
||||
_ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> = { 0 ... 0 }
|
||||
) async {
|
||||
let chat: Chat
|
||||
|
@ -32,7 +33,8 @@ func apiLoadMessages(
|
|||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
let paginationIsInitial = switch pagination { case .initial: true; default: false }
|
||||
let paginationIsLast = switch pagination { case .last: true; default: false }
|
||||
if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast) || Task.isCancelled {
|
||||
// When openAroundItemId is provided, chatId can be different too
|
||||
if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast && openAroundItemId == nil) || Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -102,8 +104,13 @@ func apiLoadMessages(
|
|||
}
|
||||
}
|
||||
case .around:
|
||||
newItems.append(contentsOf: oldItems)
|
||||
let newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
|
||||
let newSplits: [Int64]
|
||||
if openAroundItemId == nil {
|
||||
newItems.append(contentsOf: oldItems)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
|
||||
} else {
|
||||
newSplits = []
|
||||
}
|
||||
// currently, items will always be added on top, which is index 0
|
||||
newItems.insert(contentsOf: chat.chatItems, at: 0)
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
|
@ -114,8 +121,15 @@ func apiLoadMessages(
|
|||
chatState.totalAfter = navInfo.afterTotal
|
||||
chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
chatState.unreadAfter = navInfo.afterUnread
|
||||
// no need to set it, count will be wrong
|
||||
// unreadAfterNewestLoaded.value = navInfo.afterUnread
|
||||
|
||||
if let openAroundItemId {
|
||||
chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
ChatModel.shared.openAroundItemId = openAroundItemId
|
||||
ChatModel.shared.chatId = chatId
|
||||
} else {
|
||||
// no need to set it, count will be wrong
|
||||
// chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
}
|
||||
}
|
||||
case .last:
|
||||
newItems.append(contentsOf: oldItems)
|
||||
|
|
|
@ -46,7 +46,7 @@ func preloadIfNeeded(
|
|||
loadItems: @escaping (Bool, ChatPagination) async -> Bool
|
||||
) {
|
||||
let state = PreloadState.shared
|
||||
guard !listState.isScrolling,
|
||||
guard !listState.isScrolling && !listState.isAnimatedScrolling,
|
||||
state.prevFirstVisible != listState.firstVisibleItemIndex || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count,
|
||||
!state.preloading,
|
||||
listState.totalItemsCount > 0
|
||||
|
|
|
@ -29,6 +29,7 @@ struct ChatView: View {
|
|||
@State private var composeState = ComposeState()
|
||||
@State private var selectedRange = NSRange()
|
||||
@State private var keyboardVisible = false
|
||||
@State private var keyboardHiddenDate = Date.now
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var customUserProfile: Profile?
|
||||
@State private var connectionCode: String?
|
||||
|
@ -37,7 +38,7 @@ struct ChatView: View {
|
|||
@State private var requestedTopScroll = false
|
||||
@State private var loadingBottomItems = false
|
||||
@State private var requestedBottomScroll = false
|
||||
@State private var searchMode = false
|
||||
@State private var showSearch = false
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
// opening GroupMemberInfoView on member icon
|
||||
|
@ -54,11 +55,8 @@ struct ChatView: View {
|
|||
@State private var allowLoadMoreItems: Bool = false
|
||||
@State private var ignoreLoadingRequests: Int64? = nil
|
||||
@State private var animatedScrollingInProgress: Bool = false
|
||||
@State private var updateMergedItemsTask: Task<Void, Never>? = nil
|
||||
@State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel()
|
||||
|
||||
private let useItemsUpdateTask = false
|
||||
|
||||
@State private var scrollView: EndlessScrollView<MergedItem> = EndlessScrollView(frame: .zero)
|
||||
|
||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
|
@ -101,6 +99,7 @@ struct ChatView: View {
|
|||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
keyboardHiddenDate: $keyboardHiddenDate,
|
||||
selectedRange: $selectedRange
|
||||
)
|
||||
.disabled(!cInfo.sendMsgEnabled)
|
||||
|
@ -131,7 +130,7 @@ struct ChatView: View {
|
|||
}
|
||||
.safeAreaInset(edge: .top) {
|
||||
VStack(spacing: .zero) {
|
||||
if searchMode { searchToolbar() }
|
||||
if showSearch { searchToolbar() }
|
||||
Divider()
|
||||
}
|
||||
.background(ToolbarMaterial.material(toolbarMaterial))
|
||||
|
@ -232,15 +231,45 @@ struct ChatView: View {
|
|||
scrollView.listState.onUpdateListener = onChatItemsUpdated
|
||||
initChatView()
|
||||
theme = buildTheme()
|
||||
if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
closeSearch()
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
|
||||
if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] {
|
||||
scrollView.scrollToItem(index)
|
||||
} else if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
scrollView.scrollToItem(unreadIndex)
|
||||
} else {
|
||||
scrollView.scrollToBottom()
|
||||
}
|
||||
if chatModel.openAroundItemId != nil {
|
||||
chatModel.openAroundItemId = nil
|
||||
}
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.openAroundItemId) { openAround in
|
||||
if let openAround {
|
||||
closeSearch()
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
chatModel.openAroundItemId = nil
|
||||
|
||||
if let index = mergedItems.boxedValue.indexInParentItems[openAround] {
|
||||
scrollView.scrollToItem(index)
|
||||
}
|
||||
|
||||
// this may already being loading because of changed chat id (see .onChange(of: chat.id)
|
||||
if !loadingBottomItems {
|
||||
loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat)
|
||||
allowLoadMoreItems = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
allowLoadMoreItems = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
VideoPlayerView.players.removeAll()
|
||||
stopAudioPlayer()
|
||||
|
@ -429,9 +458,13 @@ struct ChatView: View {
|
|||
index = mergedItems.boxedValue.indexInParentItems[itemId]
|
||||
}
|
||||
if let index {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index))
|
||||
await MainActor.run { animatedScrollingInProgress = false }
|
||||
closeKeyboardAndRun {
|
||||
Task {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index))
|
||||
await MainActor.run { animatedScrollingInProgress = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Error scrolling to item: \(error)")
|
||||
|
@ -460,9 +493,7 @@ struct ChatView: View {
|
|||
.cornerRadius(10.0)
|
||||
|
||||
Button ("Cancel") {
|
||||
searchText = ""
|
||||
searchMode = false
|
||||
searchFocussed = false
|
||||
closeSearch()
|
||||
Task { await loadChat(chat: chat) }
|
||||
}
|
||||
}
|
||||
|
@ -517,7 +548,9 @@ struct ChatView: View {
|
|||
showChatInfoSheet: $showChatInfoSheet,
|
||||
revealedItems: $revealedItems,
|
||||
selectedChatItems: $selectedChatItems,
|
||||
forwardedChatItems: $forwardedChatItems
|
||||
forwardedChatItems: $forwardedChatItems,
|
||||
searchText: $searchText,
|
||||
closeKeyboardAndRun: closeKeyboardAndRun
|
||||
)
|
||||
// crashes on Cell size calculation without this line
|
||||
.environmentObject(ChatModel.shared)
|
||||
|
@ -535,25 +568,10 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: im.reversedChatItems) { items in
|
||||
updateMergedItemsTask?.cancel()
|
||||
if useItemsUpdateTask {
|
||||
updateMergedItemsTask = Task {
|
||||
let items = MergedItems.create(items, revealedItems, im.chatState)
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
mergedItems.boxedValue = items
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
.onChange(of: revealedItems) { revealed in
|
||||
updateMergedItemsTask?.cancel()
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
|
@ -567,6 +585,7 @@ struct ChatView: View {
|
|||
.padding(.vertical, -100)
|
||||
.onTapGesture { hideKeyboard() }
|
||||
.onChange(of: searchText) { s in
|
||||
guard showSearch else { return }
|
||||
Task {
|
||||
await loadChat(chat: chat, search: s)
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
|
@ -880,11 +899,29 @@ struct ChatView: View {
|
|||
}
|
||||
|
||||
private func focusSearch() {
|
||||
searchMode = true
|
||||
showSearch = true
|
||||
searchFocussed = true
|
||||
searchText = ""
|
||||
}
|
||||
|
||||
private func closeSearch() {
|
||||
showSearch = false
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
}
|
||||
|
||||
private func closeKeyboardAndRun(_ action: @escaping () -> Void) {
|
||||
var delay: TimeInterval = 0
|
||||
if keyboardVisible || keyboardHiddenDate.timeIntervalSinceNow >= -1 || showSearch {
|
||||
delay = 0.5
|
||||
closeSearch()
|
||||
hideKeyboard()
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
if case let .group(gInfo) = chat.chatInfo {
|
||||
|
@ -1079,6 +1116,7 @@ struct ChatView: View {
|
|||
pagination,
|
||||
im.chatState,
|
||||
searchText,
|
||||
nil,
|
||||
{ visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) }
|
||||
)
|
||||
return true
|
||||
|
@ -1136,10 +1174,14 @@ struct ChatView: View {
|
|||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
@State private var touchInProgress: Bool = false
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
@Binding var forwardedChatItems: [ChatItem]
|
||||
|
||||
@Binding var searchText: String
|
||||
var closeKeyboardAndRun: (@escaping () -> Void) -> Void
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
@State private var markedRead = false
|
||||
@State private var markReadTask: Task<Void, Never>? = nil
|
||||
|
@ -1257,6 +1299,16 @@ struct ChatView: View {
|
|||
markedRead = false
|
||||
}
|
||||
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
||||
// skip updating struct on touch if no need to show GoTo button
|
||||
.if(touchInProgress || searchIsNotBlank || (chatItem.meta.itemForwarded != nil && chatItem.meta.itemForwarded != .unknown)) {
|
||||
// long press listener steals taps from top-level listener, so repeating it's logic here as well
|
||||
$0.onTapGesture {
|
||||
hideKeyboard()
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: .infinity, perform: {}, onPressingChanged: { pressing in
|
||||
touchInProgress = pressing
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadItemIds(_ range: ClosedRange<Int>) -> ([ChatItem.ID], Int) {
|
||||
|
@ -1290,6 +1342,11 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var searchIsNotBlank: Bool {
|
||||
get {
|
||||
searchText.count > 0 && !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct MemberLayout: Layout {
|
||||
|
@ -1459,18 +1516,26 @@ struct ChatView: View {
|
|||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
ChatItemView(
|
||||
chat: chat,
|
||||
chatItem: ci,
|
||||
scrollToItemId: scrollToItemId,
|
||||
maxWidth: maxWidth,
|
||||
allowMenu: $allowMenu
|
||||
)
|
||||
.environment(\.revealed, revealed)
|
||||
.environment(\.showTimestamp, itemSeparation.timestamp)
|
||||
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
|
||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||
.accessibilityLabel("")
|
||||
HStack {
|
||||
if ci.chatDir.sent {
|
||||
goToItemButton(true)
|
||||
}
|
||||
ChatItemView(
|
||||
chat: chat,
|
||||
chatItem: ci,
|
||||
scrollToItemId: scrollToItemId,
|
||||
maxWidth: maxWidth,
|
||||
allowMenu: $allowMenu
|
||||
)
|
||||
.environment(\.revealed, revealed)
|
||||
.environment(\.showTimestamp, itemSeparation.timestamp)
|
||||
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
|
||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||
.accessibilityLabel("")
|
||||
if !ci.chatDir.sent {
|
||||
goToItemButton(false)
|
||||
}
|
||||
}
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
chatItemReactions(ci)
|
||||
.padding(.bottom, 4)
|
||||
|
@ -2133,6 +2198,37 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func goToItemInnerButton(_ alignStart: Bool, _ image: String, touchInProgress: Bool, _ onClick: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
onClick()
|
||||
} label: {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.frame(width: 13, height: 13)
|
||||
.padding([alignStart ? .trailing : .leading], 10)
|
||||
.tint(theme.colors.secondary.opacity(touchInProgress ? 1.0 : 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func goToItemButton(_ alignStart: Bool) -> some View {
|
||||
let chatTypeApiIdMsgId = chatItem.meta.itemForwarded?.chatTypeApiIdMsgId
|
||||
if searchIsNotBlank {
|
||||
goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) {
|
||||
closeKeyboardAndRun {
|
||||
ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id)
|
||||
}
|
||||
}
|
||||
} else if let chatTypeApiIdMsgId {
|
||||
goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) {
|
||||
closeKeyboardAndRun {
|
||||
let (chatType, apiId, msgId) = chatTypeApiIdMsgId
|
||||
ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SelectedChatItem: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var ciId: Int64
|
||||
|
|
|
@ -325,6 +325,7 @@ struct ComposeView: View {
|
|||
@ObservedObject var chat: Chat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var keyboardVisible: Bool
|
||||
@Binding var keyboardHiddenDate: Date
|
||||
@Binding var selectedRange: NSRange
|
||||
|
||||
@State var linkUrl: URL? = nil
|
||||
|
@ -434,6 +435,7 @@ struct ComposeView: View {
|
|||
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
|
||||
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
||||
keyboardVisible: $keyboardVisible,
|
||||
keyboardHiddenDate: $keyboardHiddenDate,
|
||||
sendButtonColor: chat.chatInfo.incognito
|
||||
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
||||
: theme.colors.primary
|
||||
|
@ -1280,6 +1282,7 @@ struct ComposeView_Previews: PreviewProvider {
|
|||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now),
|
||||
selectedRange: $selectedRange
|
||||
)
|
||||
.environmentObject(ChatModel())
|
||||
|
@ -1287,6 +1290,7 @@ struct ComposeView_Previews: PreviewProvider {
|
|||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now),
|
||||
selectedRange: $selectedRange
|
||||
)
|
||||
.environmentObject(ChatModel())
|
||||
|
|
|
@ -16,6 +16,7 @@ struct NativeTextEditor: UIViewRepresentable {
|
|||
@Binding var disableEditing: Bool
|
||||
@Binding var height: CGFloat
|
||||
@Binding var focused: Bool
|
||||
@Binding var lastUnfocusedDate: Date
|
||||
@Binding var placeholder: String?
|
||||
@Binding var selectedRange: NSRange
|
||||
let onImagesAdded: ([UploadContent]) -> Void
|
||||
|
@ -42,7 +43,12 @@ struct NativeTextEditor: UIViewRepresentable {
|
|||
onImagesAdded(images)
|
||||
}
|
||||
}
|
||||
field.setOnFocusChangedListener { focused = $0 }
|
||||
field.setOnFocusChangedListener {
|
||||
focused = $0
|
||||
if !focused {
|
||||
lastUnfocusedDate = .now
|
||||
}
|
||||
}
|
||||
field.delegate = field
|
||||
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
|
||||
field.setPlaceholderView()
|
||||
|
@ -266,6 +272,7 @@ struct NativeTextEditor_Previews: PreviewProvider{
|
|||
disableEditing: Binding.constant(false),
|
||||
height: Binding.constant(100),
|
||||
focused: Binding.constant(false),
|
||||
lastUnfocusedDate: Binding.constant(.now),
|
||||
placeholder: Binding.constant("Placeholder"),
|
||||
selectedRange: Binding.constant(NSRange(location: 0, length: 0)),
|
||||
onImagesAdded: { _ in }
|
||||
|
|
|
@ -32,6 +32,7 @@ struct SendMessageView: View {
|
|||
@State private var holdingVMR = false
|
||||
@Namespace var namespace
|
||||
@Binding var keyboardVisible: Bool
|
||||
@Binding var keyboardHiddenDate: Date
|
||||
var sendButtonColor = Color.accentColor
|
||||
@State private var teHeight: CGFloat = NativeTextEditor.minHeight
|
||||
@State private var teFont: Font = .body
|
||||
|
@ -61,6 +62,7 @@ struct SendMessageView: View {
|
|||
disableEditing: $composeState.inProgress,
|
||||
height: $teHeight,
|
||||
focused: $keyboardVisible,
|
||||
lastUnfocusedDate: $keyboardHiddenDate,
|
||||
placeholder: Binding(get: { composeState.placeholder }, set: { _ in }),
|
||||
selectedRange: $selectedRange,
|
||||
onImagesAdded: onMediaAdded
|
||||
|
@ -441,7 +443,8 @@ struct SendMessageView_Previews: PreviewProvider {
|
|||
selectedRange: $selectedRange,
|
||||
sendMessage: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: Binding.constant(true)
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now)
|
||||
)
|
||||
}
|
||||
VStack {
|
||||
|
@ -452,7 +455,8 @@ struct SendMessageView_Previews: PreviewProvider {
|
|||
selectedRange: $selectedRangeEditing,
|
||||
sendMessage: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: Binding.constant(true)
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,8 +171,10 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
visibleItems.last?.index ?? 0
|
||||
}
|
||||
|
||||
/// Whether there is scroll to item in progress or not
|
||||
/// Whether there is a non-animated scroll to item in progress or not
|
||||
var isScrolling: Bool = false
|
||||
/// Whether there is an animated scroll to item in progress or not
|
||||
var isAnimatedScrolling: Bool = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
@ -349,11 +351,11 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
if // there is auto scroll in progress and the first item has a higher offset than bottom part
|
||||
// of the screen. In order to make scrolling down & up equal in time, we treat this as a sign to
|
||||
// re-make the first visible item
|
||||
(listState.isScrolling && vis.view.frame.origin.y + vis.view.bounds.height < contentOffsetY + bounds.height) ||
|
||||
(listState.isAnimatedScrolling && vis.view.frame.origin.y + vis.view.bounds.height < contentOffsetY + bounds.height) ||
|
||||
// the fist visible item previously is hidden now, remove it and move on
|
||||
!isVisible(vis.view) {
|
||||
let newIndex: Int
|
||||
if listState.isScrolling {
|
||||
if listState.isAnimatedScrolling {
|
||||
// skip many items to make the scrolling take less time
|
||||
var indexDiff = !alreadyChangedIndexWhileScrolling ? Int(ceil(abs(offsetsDiff / averageItemHeight))) : 0
|
||||
// if index was already changed, no need to change it again. Otherwise, the scroll will overscoll and return back animated. Because it means the whole screen was scrolled
|
||||
|
@ -471,7 +473,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
}
|
||||
|
||||
func scrollToItem(_ index: Int, top: Bool = true) {
|
||||
if index >= listState.items.count || listState.isScrolling {
|
||||
if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling {
|
||||
return
|
||||
}
|
||||
if bounds.height == 0 {
|
||||
|
@ -498,7 +500,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
//let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier)
|
||||
|
||||
var stepSlowdownMultiplier: CGFloat = 1
|
||||
while true {
|
||||
while i < 200 {
|
||||
let up = index > listState.firstVisibleItemIndex
|
||||
if upPrev != up {
|
||||
stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5
|
||||
|
@ -522,18 +524,22 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
break
|
||||
}
|
||||
contentOffset = CGPointMake(contentOffset.x, adjustedOffset)
|
||||
adaptItems(listState.items, false)
|
||||
snapToContent(animated: false)
|
||||
i += 1
|
||||
}
|
||||
adaptItems(listState.items, false)
|
||||
snapToContent(animated: false)
|
||||
estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true)
|
||||
}
|
||||
|
||||
func scrollToItemAnimated(_ index: Int, top: Bool = true) async {
|
||||
if index >= listState.items.count || listState.isScrolling {
|
||||
if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling {
|
||||
return
|
||||
}
|
||||
listState.isScrolling = true
|
||||
listState.isAnimatedScrolling = true
|
||||
defer {
|
||||
listState.isScrolling = false
|
||||
listState.isAnimatedScrolling = false
|
||||
}
|
||||
var adjustedOffset = self.contentOffset.y
|
||||
var i = 0
|
||||
|
@ -543,7 +549,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
//let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier)
|
||||
|
||||
var stepSlowdownMultiplier: CGFloat = 1
|
||||
while true {
|
||||
while i < 200 {
|
||||
let up = index > listState.firstVisibleItemIndex
|
||||
if upPrev != up {
|
||||
stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5
|
||||
|
|
|
@ -20,6 +20,7 @@ struct TerminalView: View {
|
|||
@State var composeState: ComposeState = ComposeState()
|
||||
@State var selectedRange = NSRange()
|
||||
@State private var keyboardVisible = false
|
||||
@State private var keyboardHiddenDate = Date.now
|
||||
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
@State private var terminalItem: TerminalItem?
|
||||
@State private var scrolled = false
|
||||
|
@ -101,7 +102,8 @@ struct TerminalView: View {
|
|||
sendMessage: { _ in consoleSendMessage() },
|
||||
showVoiceMessageButton: false,
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
keyboardVisible: $keyboardVisible,
|
||||
keyboardHiddenDate: $keyboardHiddenDate
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
|
|
@ -3261,6 +3261,20 @@ public enum CIForwardedFrom: Decodable, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public var chatTypeApiIdMsgId: (ChatType, Int64, ChatItem.ID?)? {
|
||||
switch self {
|
||||
case .unknown: nil
|
||||
case let .contact(_, _, contactId, msgId):
|
||||
if let contactId {
|
||||
(ChatType.direct, contactId, msgId)
|
||||
} else { nil }
|
||||
case let .group(_, _, groupId, msgId):
|
||||
if let groupId {
|
||||
(ChatType.group, groupId, msgId)
|
||||
} else { nil }
|
||||
}
|
||||
}
|
||||
|
||||
public func text(_ chatType: ChatType) -> LocalizedStringKey {
|
||||
chatType == .local
|
||||
? (chatName == "" ? "saved" : "saved from \(chatName)")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue