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:
Stanislav Dmitrenko 2025-02-28 21:45:24 +07:00 committed by GitHub
parent b482d4d812
commit fefddb3b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 228 additions and 64 deletions

View file

@ -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] = []

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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 }

View file

@ -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)
)
}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)")