Compare commits

..

7 Commits

Author SHA1 Message Date
Avently
d1e2fe22ce ios: events ordering 2024-01-24 08:21:04 -08:00
Avently
be9d6de767 ui: events ordering 2024-01-24 22:24:27 +07:00
spaced4ndy
da9a7f4642 ui: exclude not ready and active contacts from list of contacts to add to group (e.g. simplex team contact) (#3737) 2024-01-24 13:56:38 +04:00
spaced4ndy
838a759a76 ui: deleted item preview (#3726) 2024-01-24 13:44:29 +04:00
spaced4ndy
f1ff27218c ui: align call buttons with calls preference (#3740) 2024-01-24 13:43:18 +04:00
spaced4ndy
8738cf332f ui: fix chat preview showing incorrect timestamp when chat is empty (#3739) 2024-01-24 13:37:29 +04:00
Evgeny Poberezkin
4f602d4571 docs: update downloads page 2024-01-23 23:22:00 +00:00
14 changed files with 85 additions and 29 deletions

View File

@@ -556,9 +556,10 @@ final class ChatModel: ObservableObject {
}
// this function analyses "connected" events and assumes that each member will be there only once
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String]) {
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String], String?) {
var count = 0
var ns: [String] = []
var lastNonConnectedEvent: String? = nil
if let ciCategory = chatItem.mergeCategory,
var i = getChatItemIndex(chatItem) {
while i < reversedChatItems.count {
@@ -566,12 +567,14 @@ final class ChatModel: ObservableObject {
if ci.mergeCategory != ciCategory { break }
if let m = ci.memberConnected {
ns.append(m.displayName)
} else if count == 0 {
lastNonConnectedEvent = if let name = ci.memberDisplayName { name + " " + ci.text } else { ci.text }
}
count += 1
i += 1
}
}
return (count, ns)
return (count, ns, lastNonConnectedEvent)
}
// returns the index of the passed item and the next item (it has smaller index)

View File

@@ -1172,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact }
.filter{ !memberContactIds.contains($0.apiId) }
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}

View File

@@ -65,6 +65,8 @@ struct MarkedDeletedItemView: View {
}
}
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
// can be refactored into a single function if functions calling these are changed to return same type
var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"

View File

@@ -149,7 +149,7 @@ struct ChatItemContentView<Content: View>: View {
}
private var mergedGroupEventText: Text? {
let (count, ns) = chatModel.getConnectedMemberNames(chatItem)
let (count, ns, lastNonConnectedEvent) = chatModel.getConnectedMemberNames(chatItem)
let members: LocalizedStringKey =
switch ns.count {
case 1: "\(ns[0]) connected"
@@ -162,6 +162,8 @@ struct ChatItemContentView<Content: View>: View {
}
return if count <= 1 {
nil
} else if let last = lastNonConnectedEvent {
Text(last) + Text(" ") + Text("and \(count - ns.count) other events")
} else if ns.count == 0 {
Text("\(count) group events")
} else if count > ns.count {

View File

@@ -159,12 +159,13 @@ struct ChatView: View {
switch cInfo {
case let .direct(contact):
HStack {
if contact.allowsFeature(.calls) {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
if callsPrefEnabled {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
}
Menu {
if contact.allowsFeature(.calls) {
if callsPrefEnabled {
Button {
CallController.shared.startCall(contact, .video)
} label: {

View File

@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
@@ -171,10 +171,21 @@ struct ChatPreviewView: View {
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
func markedDeletedText() -> String {
switch cItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
}
}
func attachment() -> String? {
switch cItem.content.msgContent {
case .file: return "doc.fill"

View File

@@ -1367,6 +1367,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public var chatTs: Date {
switch self {
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
case let .local(noteFolder): return noteFolder.chatTs
case let .contactRequest(contactRequest): return contactRequest.updatedAt
case let .contactConnection(contactConnection): return contactConnection.updatedAt
case .invalidJSON: return .now
}
}
public struct SampleData {
public var direct: ChatInfo
public var group: ChatInfo
@@ -1425,6 +1436,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var mergedPreferences: ContactUserPreferences
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool
@@ -1744,6 +1756,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
public var chatSettings: ChatSettings
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } }
@@ -2049,6 +2062,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
public var unread: Bool
var createdAt: Date
public var updatedAt: Date
var chatTs: Date
public var id: ChatId { get { "*\(noteFolderId)" } }
public var apiId: Int64 { get { noteFolderId } }
@@ -2070,7 +2084,8 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
favorite: false,
unread: false,
createdAt: .now,
updatedAt: .now
updatedAt: .now,
chatTs: .now
)
}

View File

@@ -494,10 +494,11 @@ object ChatModel {
}
// this function analyses "connected" events and assumes that each member will be there only once
fun getConnectedMemberNames(cItem: ChatItem): Pair<Int, List<String>> {
fun getConnectedMemberNames(cItem: ChatItem): Triple<Int, List<String>, String?> {
var count = 0
val ns = mutableListOf<String>()
var idx = getChatItemIndexOrNull(cItem)
var lastNonConnectedEvent: String? = null
if (cItem.mergeCategory != null && idx != null) {
val reversedChatItems = chatItems.asReversed()
while (idx < reversedChatItems.size) {
@@ -506,12 +507,14 @@ object ChatModel {
val m = ci.memberConnected
if (m != null) {
ns.add(m.displayName)
} else if (count == 0) {
lastNonConnectedEvent = if (ci.memberDisplayName != null) ci.memberDisplayName + " " + ci.text else ci.text
}
count++
idx++
}
}
return count to ns
return Triple(count, ns, lastNonConnectedEvent)
}
// returns the index of the passed item and the next item (it has smaller index)
@@ -969,6 +972,16 @@ sealed class ChatInfo: SomeChat, NamedChat {
is Group -> groupInfo.chatSettings
else -> null
}
val chatTs: Instant
get() = when(this) {
is Direct -> contact.chatTs ?: contact.updatedAt
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
is Local -> noteFolder.chatTs
is ContactRequest -> contactRequest.updatedAt
is ContactConnection -> contactConnection.updatedAt
is InvalidJSON -> updatedAt
}
}
@Serializable
@@ -1009,6 +1022,7 @@ data class Contact(
val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean
): SomeChat, NamedChat {
@@ -1077,6 +1091,7 @@ data class Contact(
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
contactGrpInvSent = false
)
}
@@ -1204,7 +1219,8 @@ data class GroupInfo (
val hostConnCustomUserProfileId: Long? = null,
val chatSettings: ChatSettings,
override val createdAt: Instant,
override val updatedAt: Instant
override val updatedAt: Instant,
val chatTs: Instant?
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -1245,7 +1261,8 @@ data class GroupInfo (
hostConnCustomUserProfileId = null,
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
)
}
}
@@ -1507,7 +1524,8 @@ class NoteFolder(
val favorite: Boolean,
val unread: Boolean,
override val createdAt: Instant,
override val updatedAt: Instant
override val updatedAt: Instant,
val chatTs: Instant
): SomeChat, NamedChat {
override val chatType get() = ChatType.Local
override val id get() = "*$noteFolderId"
@@ -1530,7 +1548,8 @@ class NoteFolder(
favorite = false,
unread = false,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
)
}
}

View File

@@ -647,7 +647,7 @@ fun ChatInfoToolbar(
}
}
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
if (activeCall == null) {
barButtons.add {
if (appPlatform.isAndroid) {

View File

@@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.map { it.chatInfo }
.filterIsInstance<ChatInfo.Direct>()
.map { it.contact }
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
.sortedBy { it.displayName.lowercase() }
.toList()
}

View File

@@ -332,7 +332,7 @@ fun ChatItemView(
}
fun mergedGroupEventText(chatItem: ChatItem): String? {
val (count, ns) = chatModel.getConnectedMemberNames(chatItem)
val (count, ns, lastNonConnectedEvent) = chatModel.getConnectedMemberNames(chatItem)
val members = when {
ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0])
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
@@ -342,6 +342,8 @@ fun ChatItemView(
}
return if (count <= 1) {
null
} else if (lastNonConnectedEvent != null) {
lastNonConnectedEvent + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size)
} else if (ns.isEmpty()) {
generalGetString(MR.strings.rcv_group_events_count).format(count)
} else if (count > ns.size) {

View File

@@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
)
}
private fun markedDeletedText(meta: CIMeta): String =
fun markedDeletedText(meta: CIMeta): String =
when (meta.itemDeleted) {
is CIDeleted.Moderated ->
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)

View File

@@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.item.markedDeletedText
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -170,7 +171,7 @@ fun ChatPreviewView(
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> generalGetString(MR.strings.marked_deleted_description) to null
else -> markedDeletedText(ci.meta) to null
}
val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
@@ -286,7 +287,7 @@ fun ChatPreviewView(
Box(
contentAlignment = Alignment.TopEnd
) {
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
Text(
ts,
color = MaterialTheme.colors.secondary,

View File

@@ -7,7 +7,7 @@ revision: 25.11.2023
| Updated 25.11.2023 | Languages: EN |
# Download SimpleX apps
The latest stable version is v5.4.3.
The latest stable version is v5.5.
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
@@ -21,24 +21,24 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch
Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps.
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-ubuntu-22_04-x86_64.deb).
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-22_04-x86_64.deb).
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-desktop-windows-x86_64.msi).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-windows-x86_64.msi).
## Mobile apps
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-armv7a.apk).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-armv7a.apk).
## Terminal (console) app
See [Using terminal app](/docs/CLI.md).
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-chat-ubuntu-22_04-x86-64).
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-22_04-x86-64).
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#).
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.3/simplex-chat-windows-x86-64).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-windows-x86-64).