mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5090c3df2 | |||
| 71a3c511eb | |||
| 78e27f3296 | |||
| d064fe00b1 | |||
| 28fc268e7f | |||
| d76b321919 | |||
| 299766a758 | |||
| 86fd8cf5a1 | |||
| 46d7195615 | |||
| 5b18da4fe6 | |||
| 4005da2524 | |||
| 7e12dad716 | |||
| bd2d751a81 | |||
| 1321a198f3 | |||
| 6777944cec | |||
| 1f2c4ecb17 | |||
| 1f18fca722 |
@@ -50,9 +50,6 @@ class ItemsModel: ObservableObject {
|
||||
var reversedChatItems: [ChatItem] = [] {
|
||||
willSet { publisher.send() }
|
||||
}
|
||||
var itemAdded = false {
|
||||
willSet { publisher.send() }
|
||||
}
|
||||
|
||||
// Publishes directly to `objectWillChange` publisher,
|
||||
// this will cause reversedChatItems to be rendered without throttling
|
||||
@@ -456,7 +453,6 @@ final class ChatModel: ObservableObject {
|
||||
ci.meta.itemStatus = status
|
||||
}
|
||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
im.itemAdded = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -551,7 +547,6 @@ final class ChatModel: ObservableObject {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
im.reversedChatItems.insert(cItem, at: 0)
|
||||
im.itemAdded = true
|
||||
}
|
||||
return cItem
|
||||
}
|
||||
@@ -902,22 +897,14 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
|
||||
var i = 0
|
||||
var totalBelow = 0
|
||||
var unreadBelow = 0
|
||||
while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
|
||||
totalBelow += 1
|
||||
if im.reversedChatItems[i].isRcvNew {
|
||||
unreadBelow += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return UnreadChatItemCounts(
|
||||
// TODO these thresholds account for the fact that items are still "visible" while
|
||||
// covered by compose area, they should be replaced with the actual height in pixels below the screen.
|
||||
isNearBottom: totalBelow < 15,
|
||||
isReallyNearBottom: totalBelow < 2,
|
||||
unreadBelow: unreadBelow
|
||||
)
|
||||
return UnreadChatItemCounts(unreadBelow: unreadBelow)
|
||||
}
|
||||
|
||||
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
||||
@@ -941,8 +928,6 @@ struct NTFContactRequest {
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var isNearBottom: Bool
|
||||
var isReallyNearBottom: Bool
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
|
||||
@@ -1420,9 +1420,9 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) {
|
||||
func getAgentSubsTotal() throws -> (SMPServerSubs, Bool) {
|
||||
let userId = try currentUserId("getAgentSubsTotal")
|
||||
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId))
|
||||
let r = chatSendCmdSync(.getAgentSubsTotal(userId: userId), log: false)
|
||||
if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) }
|
||||
logger.error("getAgentSubsTotal error: \(String(describing: r))")
|
||||
throw r
|
||||
|
||||
@@ -395,7 +395,11 @@ struct ChatView: View {
|
||||
let cInfo = chat.chatInfo
|
||||
let mergedItems = filtered(im.reversedChatItems)
|
||||
return GeometryReader { g in
|
||||
ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in
|
||||
ReverseList(
|
||||
items: mergedItems,
|
||||
scrollState: $scrollModel.state,
|
||||
isFarFromBottom: $scrollModel.isFarFromBottom
|
||||
) { ci in
|
||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||
let maxWidth = cInfo.chatType == .group
|
||||
? voiceNoFrame
|
||||
@@ -432,14 +436,6 @@ struct ChatView: View {
|
||||
.onChange(of: im.reversedChatItems) { _ in
|
||||
floatingButtonModel.chatItemsChanged()
|
||||
}
|
||||
.onChange(of: im.itemAdded) { added in
|
||||
if added {
|
||||
im.itemAdded = false
|
||||
if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom {
|
||||
scrollModel.scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,11 +466,7 @@ struct ChatView: View {
|
||||
private var bag = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
unreadChatItemCounts = UnreadChatItemCounts(
|
||||
isNearBottom: true,
|
||||
isReallyNearBottom: true,
|
||||
unreadBelow: 0
|
||||
)
|
||||
unreadChatItemCounts = UnreadChatItemCounts(unreadBelow: 0)
|
||||
events
|
||||
.receive(on: DispatchQueue.global(qos: .background))
|
||||
.scan(Set<String>()) { itemsInView, event in
|
||||
@@ -538,7 +530,7 @@ struct ChatView: View {
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToBottom()
|
||||
}
|
||||
} else if !counts.isNearBottom {
|
||||
} else if scrollModel.isFarFromBottom {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
let items: Array<Item>
|
||||
|
||||
@Binding var scrollState: ReverseListScrollModel<Item>.State
|
||||
@Binding var isFarFromBottom: Bool
|
||||
|
||||
/// Closure, that returns user interface for a given item
|
||||
let content: (Item) -> Content
|
||||
@@ -163,24 +164,90 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
animated: animated
|
||||
)
|
||||
} else {
|
||||
tableView.setContentOffset(
|
||||
CGPoint(x: .zero, y: -InvertedTableView.inset),
|
||||
animated: animated
|
||||
)
|
||||
scrollToBottom(animated: animated)
|
||||
}
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
|
||||
func update(items: Array<Item>) {
|
||||
private func scrollToBottom(animated: Bool) {
|
||||
tableView.setContentOffset(
|
||||
CGPoint(x: .zero, y: -InvertedTableView.inset),
|
||||
animated: animated
|
||||
)
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let adjustedOffset = tableView.contentOffset.y + InvertedTableView.inset
|
||||
if representer.isFarFromBottom {
|
||||
if adjustedOffset < 800 {
|
||||
DispatchQueue.main.async { self.representer.isFarFromBottom = false }
|
||||
}
|
||||
} else if adjustedOffset > 1000 {
|
||||
DispatchQueue.main.async { self.representer.isFarFromBottom = true }
|
||||
}
|
||||
}
|
||||
|
||||
private var isNearBottom: Bool { scrollOffset > 0 && scrollOffset < 500 }
|
||||
|
||||
private var isAtBottom: Bool { scrollOffset == 0 }
|
||||
|
||||
private var scrollOffset: CGFloat { tableView.contentOffset.y + InvertedTableView.inset }
|
||||
|
||||
private var itemId: Item.ID?
|
||||
private var retainedItems: [Item]?
|
||||
private var updateInProgress = false
|
||||
|
||||
func update(items: [Item]) {
|
||||
if updateInProgress {
|
||||
retainedItems = items
|
||||
return
|
||||
}
|
||||
if let itemId,
|
||||
let i = items.firstIndex(where: { $0.id == itemId }),
|
||||
i > 0 {
|
||||
updateInProgress = true
|
||||
// Update existing items without animation
|
||||
_update(items: Array(items[i...])) {
|
||||
DispatchQueue.main.async {
|
||||
// Added items animated by sliding from bottom (.top)
|
||||
self._update(
|
||||
items: items,
|
||||
animated: self.isAtBottom,
|
||||
rowAnimation: self.isAtBottom ? .top : .none
|
||||
) {
|
||||
self.updateInProgress = false
|
||||
if self.isNearBottom { self.scrollToBottom(animated: false) }
|
||||
// Process update, which might have arrived before completion
|
||||
if let items = self.retainedItems {
|
||||
self.retainedItems = nil
|
||||
self.update(items: items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let wasItemsRemoved = !items.isEmpty && items.count < itemCount
|
||||
_update(
|
||||
items: items,
|
||||
animated: wasItemsRemoved,
|
||||
rowAnimation: wasItemsRemoved ? .fade : .none
|
||||
)
|
||||
}
|
||||
itemId = items.first?.id
|
||||
itemCount = items.count
|
||||
}
|
||||
|
||||
private func _update(
|
||||
items: [Item],
|
||||
animated: Bool = false,
|
||||
rowAnimation: UITableView.RowAnimation = .none,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.defaultRowAnimation = .none
|
||||
dataSource.apply(
|
||||
snapshot,
|
||||
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
|
||||
)
|
||||
itemCount = items.count
|
||||
dataSource.defaultRowAnimation = rowAnimation
|
||||
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +306,7 @@ class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
|
||||
}
|
||||
|
||||
@Published var state: State = .atDestination
|
||||
@Published var isFarFromBottom: Bool = false
|
||||
|
||||
func scrollToNextPage() {
|
||||
state = .scrollingTo(.nextPage)
|
||||
|
||||
@@ -324,7 +324,7 @@ struct ChatListView: View {
|
||||
struct SubsStatusIndicator: View {
|
||||
@State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs
|
||||
@State private var hasSess: Bool = false
|
||||
@State private var task: Task<Void, Never>?
|
||||
@State private var timer: Timer? = nil
|
||||
@State private var showServersSummary = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||
@@ -343,10 +343,10 @@ struct SubsStatusIndicator: View {
|
||||
}
|
||||
.disabled(ChatModel.shared.chatRunning != true)
|
||||
.onAppear {
|
||||
startTask()
|
||||
startTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopTask()
|
||||
stopTimer()
|
||||
}
|
||||
.appSheet(isPresented: $showServersSummary) {
|
||||
ServersSummaryView()
|
||||
@@ -354,28 +354,25 @@ struct SubsStatusIndicator: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func startTask() {
|
||||
task = Task {
|
||||
while !Task.isCancelled {
|
||||
if AppChatState.shared.value == .active {
|
||||
do {
|
||||
let (subs, hasSess) = try await getAgentSubsTotal()
|
||||
await MainActor.run {
|
||||
self.subs = subs
|
||||
self.hasSess = hasSess
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("getSubsTotal error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second
|
||||
private func startTimer() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
if AppChatState.shared.value == .active {
|
||||
getSubsTotal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopTask() {
|
||||
task?.cancel()
|
||||
task = nil
|
||||
func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func getSubsTotal() {
|
||||
do {
|
||||
(subs, hasSess) = try getAgentSubsTotal()
|
||||
} catch let error {
|
||||
logger.error("getSubsTotal error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: f229e135e33ec5ee1f6d0978bd903d84b0b60efa
|
||||
tag: 0dd52dc69f197a86f2f03e47b339bf3a94a596a3
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."f229e135e33ec5ee1f6d0978bd903d84b0b60efa" = "0p4n4bghg24a98py9mij6s24fvsix4a73lwkg6skqf4rx8zp392v";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."0dd52dc69f197a86f2f03e47b339bf3a94a596a3" = "1dzfpk9bq8y885i2z419k79f2nyagrqkqh8rw3gzmf1hy5w3fbjs";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
Reference in New Issue
Block a user