mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4947cf2c0c | |||
| 2e205b055d |
@@ -11,38 +11,6 @@ import Combine
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
actor TerminalItems {
|
||||
private var terminalItems: [TerminalItem] = []
|
||||
|
||||
static let shared = TerminalItems()
|
||||
|
||||
func items() -> [TerminalItem] {
|
||||
terminalItems
|
||||
}
|
||||
|
||||
func add(_ item: TerminalItem) async {
|
||||
addTermItem(&terminalItems, item)
|
||||
let m = ChatModel.shared
|
||||
if m.showingTerminal {
|
||||
await MainActor.run {
|
||||
addTermItem(&m.terminalItems, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async {
|
||||
addTermItem(&terminalItems, .cmd(start, cmd))
|
||||
addTermItem(&terminalItems, .resp(.now, resp))
|
||||
}
|
||||
}
|
||||
|
||||
private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
||||
if items.count >= 200 {
|
||||
items.removeFirst()
|
||||
}
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var setDeliveryReceipts = false
|
||||
@@ -65,7 +33,6 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GroupMember] = []
|
||||
// items in the terminal view
|
||||
@Published var showingTerminal = false
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: UserContactLink?
|
||||
@Published var chatItemTTL: ChatItemTTL = .none
|
||||
@@ -516,27 +483,14 @@ final class ChatModel: ObservableObject {
|
||||
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
|
||||
}
|
||||
|
||||
func getConnectedMemberNames(_ ci: ChatItem) -> [String] {
|
||||
guard var i = getChatItemIndex(ci) else { return [] }
|
||||
var ns: [String] = []
|
||||
while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected {
|
||||
ns.append(m.displayName)
|
||||
i += 1
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) {
|
||||
if let i = getChatItemIndex(ci) {
|
||||
return (
|
||||
i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil,
|
||||
i - 1 >= 0 ? reversedChatItems[i - 1] : nil
|
||||
)
|
||||
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
||||
if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 {
|
||||
return reversedChatItems[i + 1]
|
||||
} else {
|
||||
return (nil, nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func popChat(_ id: String) {
|
||||
if let i = getChatIndex(id) {
|
||||
popChat_(i)
|
||||
@@ -625,6 +579,13 @@ final class ChatModel: ObservableObject {
|
||||
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
||||
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
||||
}
|
||||
|
||||
func addTerminalItem(_ item: TerminalItem) {
|
||||
if terminalItems.count >= 500 {
|
||||
terminalItems.remove(at: 0)
|
||||
}
|
||||
terminalItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
struct NTFContactRequest {
|
||||
|
||||
@@ -94,8 +94,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated))
|
||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -320,18 +321,12 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
var cItem: ChatItem? = nil
|
||||
let endTask = beginBGTask({
|
||||
if let cItem = cItem {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.messageDelivery.removeValue(forKey: cItem.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
var cItem: ChatItem!
|
||||
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
||||
r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
|
||||
chatModel.messageDelivery[cItem.id] = endTask
|
||||
return cItem
|
||||
}
|
||||
if let networkErrorAlert = networkErrorAlert(r) {
|
||||
@@ -809,7 +804,7 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
|
||||
func receiveFile(user: User, fileId: Int64, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,7 +842,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) asyn
|
||||
|
||||
func cancelFile(user: User, fileId: Int64) async {
|
||||
if let chatItem = await apiCancelFile(fileId: fileId) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
||||
cleanupFile(chatItem)
|
||||
}
|
||||
}
|
||||
@@ -1249,50 +1244,38 @@ class ChatReceiver {
|
||||
}
|
||||
|
||||
func processReceivedMsg(_ res: ChatResponse) async {
|
||||
Task {
|
||||
await TerminalItems.shared.add(.resp(.now, res))
|
||||
}
|
||||
let m = ChatModel.shared
|
||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .newContactConnection(user, connection):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
await MainActor.run {
|
||||
m.addTerminalItem(.resp(.now, res))
|
||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .newContactConnection(user, connection):
|
||||
if active(user) {
|
||||
m.updateContactConnection(connection)
|
||||
}
|
||||
}
|
||||
case let .contactConnectionDeleted(user, connection):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .contactConnectionDeleted(user, connection):
|
||||
if active(user) {
|
||||
m.removeChat(connection.id)
|
||||
}
|
||||
}
|
||||
case let .contactConnected(user, contact, _):
|
||||
if active(user) && contact.directOrUsed {
|
||||
await MainActor.run {
|
||||
case let .contactConnected(user, contact, _):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
}
|
||||
if contact.directOrUsed {
|
||||
NtfManager.shared.notifyContactConnected(user, contact)
|
||||
}
|
||||
await MainActor.run {
|
||||
if contact.directOrUsed {
|
||||
NtfManager.shared.notifyContactConnected(user, contact)
|
||||
}
|
||||
m.setContactNetworkStatus(contact, .connected)
|
||||
}
|
||||
case let .contactConnecting(user, contact):
|
||||
if active(user) && contact.directOrUsed {
|
||||
await MainActor.run {
|
||||
case let .contactConnecting(user, contact):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
}
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
if active(user) {
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
await MainActor.run {
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
if active(user) {
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
} else {
|
||||
@@ -1302,285 +1285,234 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
await MainActor.run {
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
m.updateChatInfo(cInfo)
|
||||
}
|
||||
}
|
||||
case let .contactsMerged(user, intoContact, mergedContact):
|
||||
if active(user) && m.hasChat(mergedContact.id) {
|
||||
await MainActor.run {
|
||||
case let .contactsMerged(user, intoContact, mergedContact):
|
||||
if active(user) && m.hasChat(mergedContact.id) {
|
||||
if m.chatId == mergedContact.id {
|
||||
m.chatId = intoContact.id
|
||||
}
|
||||
m.removeChat(mergedContact.id)
|
||||
}
|
||||
}
|
||||
case let .contactsSubscribed(_, contactRefs):
|
||||
await updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
await updateContactsStatus(contactRefs, status: .disconnected)
|
||||
case let .contactSubError(user, contact, chatError):
|
||||
await MainActor.run {
|
||||
case let .contactsSubscribed(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .disconnected)
|
||||
case let .contactSubError(user, contact, chatError):
|
||||
if active(user) {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
processContactSubError(contact, chatError)
|
||||
}
|
||||
case let .contactSubSummary(_, contactSubscriptions):
|
||||
await MainActor.run {
|
||||
case let .contactSubSummary(user, contactSubscriptions):
|
||||
for sub in contactSubscriptions {
|
||||
// no need to update contact here, and it is slow
|
||||
// if active(user) {
|
||||
// m.updateContact(sub.contact)
|
||||
// }
|
||||
if active(user) {
|
||||
m.updateContact(sub.contact)
|
||||
}
|
||||
if let err = sub.contactError {
|
||||
processContactSubError(sub.contact, err)
|
||||
} else {
|
||||
m.setContactNetworkStatus(sub.contact, .connected)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .newChatItem(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
await MainActor.run {
|
||||
case let .newChatItem(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if active(user) {
|
||||
m.addChatItem(cInfo, cItem)
|
||||
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
||||
m.increaseUnreadCounter(user: user)
|
||||
}
|
||||
}
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
Task {
|
||||
await receiveFile(user: user, fileId: file.fileId, auto: true)
|
||||
}
|
||||
}
|
||||
if cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
case let .chatItemStatusUpdated(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if !cItem.isDeletedContent {
|
||||
let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
|
||||
if added && cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndSent: endTask()
|
||||
case .sndErrorAuth: endTask()
|
||||
case .sndError: endTask()
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
case let .chatItemUpdated(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .chatItemReaction(user, _, r):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||
if !active(user) {
|
||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||
await MainActor.run {
|
||||
m.decreaseUnreadCounter(user: user)
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
Task {
|
||||
await receiveFile(user: user, fileId: file.fileId, auto: true)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
case let .chatItemStatusUpdated(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if !cItem.isDeletedContent {
|
||||
let added = active(user) ? m.upsertChatItem(cInfo, cItem) : true
|
||||
if added && cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndSent: endTask()
|
||||
case .sndErrorAuth: endTask()
|
||||
case .sndError: endTask()
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
case let .chatItemUpdated(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .chatItemReaction(user, _, r):
|
||||
if active(user) {
|
||||
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
|
||||
}
|
||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||
if !active(user) {
|
||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||
m.decreaseUnreadCounter(user: user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if let toChatItem = toChatItem {
|
||||
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
|
||||
} else {
|
||||
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
}
|
||||
}
|
||||
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
||||
if !active(user) { return }
|
||||
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
||||
if !active(user) { return }
|
||||
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
if let hostContact = hostContact {
|
||||
m.dismissConnReqView(hostContact.activeConn.id)
|
||||
m.removeChat(hostContact.activeConn.id)
|
||||
}
|
||||
}
|
||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
case let .deletedMember(user, groupInfo, _, deletedMember):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .deletedMember(user, groupInfo, _, deletedMember):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
}
|
||||
}
|
||||
case let .leftMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .leftMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
case let .userJoinedGroup(user, groupInfo):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .userJoinedGroup(user, groupInfo):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
case let .joinedGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .joinedGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
if let contact = memberContact {
|
||||
await MainActor.run {
|
||||
if let contact = memberContact {
|
||||
m.setContactNetworkStatus(contact, .connected)
|
||||
}
|
||||
}
|
||||
case let .groupUpdated(user, toGroup):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .groupUpdated(user, toGroup):
|
||||
if active(user) {
|
||||
m.updateGroup(toGroup)
|
||||
}
|
||||
}
|
||||
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileError(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupDirectFile(aChatItem) }
|
||||
case let .sndFileRcvCancelled(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupDirectFile(aChatItem) }
|
||||
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .sndFileError(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .callInvitation(invitation):
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
activateCall(invitation)
|
||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||
await withCall(contact) { call in
|
||||
call.callState = .offerReceived
|
||||
call.peerMedia = callType.media
|
||||
call.sharedKey = sharedKey
|
||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
let iceServers = getIceServers()
|
||||
logger.debug(".callOffer useRelay \(useRelay)")
|
||||
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
||||
m.callCommand = .offer(
|
||||
offer: offer.rtcSession,
|
||||
iceCandidates: offer.rtcIceCandidates,
|
||||
media: callType.media, aesKey: sharedKey,
|
||||
iceServers: iceServers,
|
||||
relay: useRelay
|
||||
)
|
||||
}
|
||||
case let .callAnswer(_, contact, answer):
|
||||
await withCall(contact) { call in
|
||||
call.callState = .answerReceived
|
||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
||||
}
|
||||
case let .callExtraInfo(_, contact, extraInfo):
|
||||
await withCall(contact) { _ in
|
||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
||||
}
|
||||
case let .callEnded(_, contact):
|
||||
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||
}
|
||||
await withCall(contact) { call in
|
||||
m.callCommand = .end
|
||||
CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
await MainActor.run {
|
||||
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileError(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupDirectFile(aChatItem)
|
||||
case let .sndFileRcvCancelled(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupDirectFile(aChatItem)
|
||||
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .sndFileError(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .callInvitation(invitation):
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
activateCall(invitation)
|
||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||
withCall(contact) { call in
|
||||
call.callState = .offerReceived
|
||||
call.peerMedia = callType.media
|
||||
call.sharedKey = sharedKey
|
||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
let iceServers = getIceServers()
|
||||
logger.debug(".callOffer useRelay \(useRelay)")
|
||||
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
||||
m.callCommand = .offer(
|
||||
offer: offer.rtcSession,
|
||||
iceCandidates: offer.rtcIceCandidates,
|
||||
media: callType.media, aesKey: sharedKey,
|
||||
iceServers: iceServers,
|
||||
relay: useRelay
|
||||
)
|
||||
}
|
||||
case let .callAnswer(_, contact, answer):
|
||||
withCall(contact) { call in
|
||||
call.callState = .answerReceived
|
||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
||||
}
|
||||
case let .callExtraInfo(_, contact, extraInfo):
|
||||
withCall(contact) { _ in
|
||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
||||
}
|
||||
case let .callEnded(_, contact):
|
||||
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
|
||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||
}
|
||||
withCall(contact) { call in
|
||||
m.callCommand = .end
|
||||
CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
}
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
await MainActor.run {
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
}
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
|
||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
|
||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||
await MainActor.run { perform(call) }
|
||||
} else {
|
||||
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) {
|
||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||
perform(call)
|
||||
} else {
|
||||
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1589,23 +1521,19 @@ func active(_ user: User) -> Bool {
|
||||
user.id == ChatModel.shared.currentUser?.id
|
||||
}
|
||||
|
||||
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) async {
|
||||
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) {
|
||||
let m = ChatModel.shared
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if active(user) {
|
||||
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
if active(user) && m.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
|
||||
let m = ChatModel.shared
|
||||
await MainActor.run {
|
||||
for c in contactRefs {
|
||||
m.networkStatuses[c.agentConnId] = status
|
||||
}
|
||||
for c in contactRefs {
|
||||
m.networkStatuses[c.agentConnId] = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1644,9 +1572,7 @@ func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
let m = ChatModel.shared
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||
}
|
||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.debug("reportNewIncomingCall success")
|
||||
|
||||
@@ -10,11 +10,20 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIEventView: View {
|
||||
var eventText: Text
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
eventText
|
||||
if let member = chatItem.memberDisplayName {
|
||||
Text(member)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
+ Text(" ")
|
||||
+ chatEventText(chatItem)
|
||||
} else {
|
||||
chatEventText(chatItem)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.bottom, 6)
|
||||
@@ -22,8 +31,20 @@ struct CIEventView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem) -> Text {
|
||||
Text(ci.content.text)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
+ Text(" ")
|
||||
+ ci.timestampText
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
|
||||
struct CIEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIEventView(eventText: Text("event happened"))
|
||||
CIEventView(chatItem: ChatItem.getGroupEventSample())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ struct CIRcvDecryptionError: View {
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
@State private var alert: CIRcvDecryptionErrorAlert?
|
||||
|
||||
enum CIRcvDecryptionErrorAlert: Identifiable {
|
||||
@@ -105,6 +106,9 @@ struct CIRcvDecryptionError: View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ")
|
||||
}
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
@@ -133,13 +137,20 @@ struct CIRcvDecryptionError: View {
|
||||
}
|
||||
|
||||
private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View {
|
||||
func text() -> Text {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
|
||||
}
|
||||
return ZStack(alignment: .bottomTrailing) {
|
||||
HStack {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ") + text()
|
||||
} else {
|
||||
text()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
|
||||
@@ -12,9 +12,13 @@ import SimpleXChat
|
||||
struct DeletedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ")
|
||||
}
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
@@ -33,7 +37,10 @@ struct DeletedItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
|
||||
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
|
||||
DeletedItemView(
|
||||
chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)),
|
||||
showMember: true
|
||||
)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ struct FramedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatInfo: ChatInfo
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@State var msgWidth: CGFloat = 0
|
||||
@@ -56,7 +57,7 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
|
||||
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: framedMsgContentView)
|
||||
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: framedMsgContentView)
|
||||
.padding(chatItem.content.msgContent != nil ? 0 : 4)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -67,7 +68,6 @@ struct FramedItemView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
@@ -107,7 +107,7 @@ struct FramedItemView: View {
|
||||
value: .white
|
||||
)
|
||||
} else {
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .video(text, image, duration):
|
||||
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
|
||||
@@ -120,27 +120,27 @@ struct FramedItemView: View {
|
||||
value: .white
|
||||
)
|
||||
} else {
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .voice(text, duration):
|
||||
FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
.overlay(DetermineWidth())
|
||||
if text != "" {
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .file(text):
|
||||
ciFileView(chatItem, text)
|
||||
case let .link(_, preview):
|
||||
CILinkView(linkPreview: preview)
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
case let .unknown(_, text: text):
|
||||
if chatItem.file == nil {
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
} else {
|
||||
ciFileView(chatItem, text)
|
||||
}
|
||||
default:
|
||||
ciMsgContentView(chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,27 +232,17 @@ struct FramedItemView: View {
|
||||
}
|
||||
|
||||
private func ciQuotedMsgView(_ qi: CIQuote) -> some View {
|
||||
Group {
|
||||
if let sender = qi.getSender(membership()) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(sender).font(.caption).foregroundColor(.secondary)
|
||||
ciQuotedMsgTextView(qi, lines: 2)
|
||||
}
|
||||
} else {
|
||||
ciQuotedMsgTextView(qi, lines: 3)
|
||||
}
|
||||
}
|
||||
.padding(.top, 6)
|
||||
MsgContentView(
|
||||
text: qi.text,
|
||||
formattedText: qi.formattedText,
|
||||
sender: qi.getSender(membership())
|
||||
)
|
||||
.lineLimit(3)
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
|
||||
MsgContentView(text: qi.text, formattedText: qi.formattedText)
|
||||
.lineLimit(lines)
|
||||
.font(.subheadline)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
|
||||
private func ciQuoteIconView(_ image: String) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
@@ -270,12 +260,13 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View {
|
||||
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ showMember: Bool = false) -> some View {
|
||||
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
|
||||
let rtl = isRightToLeft(text)
|
||||
let v = MsgContentView(
|
||||
text: text,
|
||||
formattedText: text == "" ? [] : ci.formattedText,
|
||||
sender: showMember ? ci.memberDisplayName : nil,
|
||||
meta: ci.meta,
|
||||
rightToLeft: rtl
|
||||
)
|
||||
@@ -297,7 +288,7 @@ struct FramedItemView: View {
|
||||
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
|
||||
.overlay(DetermineWidth())
|
||||
if text != "" || ci.meta.isLive {
|
||||
ciMsgContentView (chatItem)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ import SimpleXChat
|
||||
struct IntegrityErrorItemView: View {
|
||||
var msgError: MsgErrorType
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
|
||||
var body: some View {
|
||||
CIMsgError(chatItem: chatItem) {
|
||||
CIMsgError(chatItem: chatItem, showMember: showMember) {
|
||||
switch msgError {
|
||||
case .msgSkipped:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -53,10 +54,14 @@ struct IntegrityErrorItemView: View {
|
||||
|
||||
struct CIMsgError: View {
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
var onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).fontWeight(.medium) + Text(": ")
|
||||
}
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
|
||||
@@ -12,9 +12,13 @@ import SimpleXChat
|
||||
struct MarkedDeletedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption)
|
||||
}
|
||||
if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
|
||||
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ChatItemView: View {
|
||||
var chatInfo: ChatInfo
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@Binding var revealed: Bool
|
||||
@@ -22,6 +23,7 @@ struct ChatItemView: View {
|
||||
init(chatInfo: ChatInfo, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, scrollProxy: ScrollViewProxy? = nil, revealed: Binding<Bool>, allowMenu: Binding<Bool> = .constant(false), audioPlayer: Binding<AudioPlayer?> = .constant(nil), playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), playbackTime: Binding<TimeInterval?> = .constant(nil)) {
|
||||
self.chatInfo = chatInfo
|
||||
self.chatItem = chatItem
|
||||
self.showMember = showMember
|
||||
self.maxWidth = maxWidth
|
||||
_scrollProxy = .init(initialValue: scrollProxy)
|
||||
_revealed = revealed
|
||||
@@ -34,14 +36,14 @@ struct ChatItemView: View {
|
||||
var body: some View {
|
||||
let ci = chatItem
|
||||
if chatItem.meta.itemDeleted != nil && !revealed {
|
||||
MarkedDeletedItemView(chatItem: chatItem)
|
||||
MarkedDeletedItemView(chatItem: chatItem, showMember: showMember)
|
||||
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
||||
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
||||
EmojiItemView(chatItem: ci)
|
||||
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
||||
CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
|
||||
} else if ci.content.msgContent == nil {
|
||||
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -51,14 +53,14 @@ struct ChatItemView: View {
|
||||
}
|
||||
|
||||
private func framedItemView() -> some View {
|
||||
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatItemContentView<Content: View>: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var chatInfo: ChatInfo
|
||||
var chatItem: ChatItem
|
||||
var showMember: Bool
|
||||
var msgContentView: () -> Content
|
||||
|
||||
var body: some View {
|
||||
@@ -69,11 +71,10 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case .rcvDeleted: deletedItemView()
|
||||
case let .sndCall(status, duration): callItemView(status, duration)
|
||||
case let .rcvCall(status, duration): callItemView(status, duration)
|
||||
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem)
|
||||
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
|
||||
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem, showMember: showMember)
|
||||
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember)
|
||||
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
|
||||
case .rcvGroupEvent: eventItemView()
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
@@ -95,7 +96,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func deletedItemView() -> some View {
|
||||
DeletedItemView(chatItem: chatItem)
|
||||
DeletedItemView(chatItem: chatItem, showMember: showMember)
|
||||
}
|
||||
|
||||
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
|
||||
@@ -107,54 +108,12 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func eventItemView() -> some View {
|
||||
return CIEventView(eventText: eventItemViewText())
|
||||
}
|
||||
|
||||
private func eventItemViewText() -> Text {
|
||||
if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " ")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
+ chatEventText(chatItem)
|
||||
} else {
|
||||
return chatEventText(chatItem)
|
||||
}
|
||||
CIEventView(chatItem: chatItem)
|
||||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var membersConnectedItemText: Text {
|
||||
if let t = membersConnectedText {
|
||||
return chatEventText(t, chatItem.timestampText)
|
||||
} else {
|
||||
return eventItemViewText()
|
||||
}
|
||||
}
|
||||
|
||||
private var membersConnectedText: LocalizedStringKey? {
|
||||
let ns = chatModel.getConnectedMemberNames(chatItem)
|
||||
return ns.count > 3
|
||||
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
|
||||
: ns.count == 3
|
||||
? "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
|
||||
: ns.count == 2
|
||||
? "\(ns[0]) and \(ns[1]) connected"
|
||||
: nil
|
||||
}
|
||||
}
|
||||
|
||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
|
||||
(Text(eventText) + Text(" ") + ts)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem) -> Text {
|
||||
chatEventText("\(ci.content.text)", ci.timestampText)
|
||||
}
|
||||
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -261,7 +261,7 @@ struct ChatView: View {
|
||||
return GeometryReader { g in
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
LazyVStack(spacing: 5) {
|
||||
ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in
|
||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||
let maxWidth = cInfo.chatType == .group
|
||||
@@ -430,77 +430,68 @@ struct ChatView: View {
|
||||
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
if case let .groupRcv(member) = ci.chatDir,
|
||||
case let .group(groupInfo) = chat.chatInfo {
|
||||
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
|
||||
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
|
||||
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
|
||||
ZStack {} // scroll doesn't work if it's EmptyView()
|
||||
} else {
|
||||
if prevItem == nil || showMemberImage(member, prevItem) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
Text(member.displayName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, memberImageSize + 14)
|
||||
.padding(.top, 7)
|
||||
let prevItem = chatModel.getPrevChatItem(ci)
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
let showMember = prevItem == nil || showMemberImage(member, prevItem)
|
||||
if showMember {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
.onTapGesture { selectedMember = member }
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
.onTapGesture { selectedMember = member }
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
|
||||
}
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
}
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
.padding(.top, 5)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, memberImageSize + 8 + 12)
|
||||
Rectangle().fill(.clear)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
}
|
||||
ChatItemWithMenu(
|
||||
ci: ci,
|
||||
showMember: showMember,
|
||||
maxWidth: maxWidth,
|
||||
scrollProxy: scrollProxy,
|
||||
deleteMessage: deleteMessage,
|
||||
deletingItem: $deletingItem,
|
||||
composeState: $composeState,
|
||||
showDeleteMessage: $showDeleteMessage
|
||||
)
|
||||
.padding(.leading, 8)
|
||||
.environmentObject(chat)
|
||||
}
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
ChatItemWithMenu(
|
||||
ci: ci,
|
||||
maxWidth: maxWidth,
|
||||
scrollProxy: scrollProxy,
|
||||
deleteMessage: deleteMessage,
|
||||
deletingItem: $deletingItem,
|
||||
composeState: $composeState,
|
||||
showDeleteMessage: $showDeleteMessage
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.environmentObject(chat)
|
||||
}
|
||||
}
|
||||
|
||||
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
ChatItemWithMenu(
|
||||
ci: ci,
|
||||
maxWidth: maxWidth,
|
||||
scrollProxy: scrollProxy,
|
||||
deleteMessage: deleteMessage,
|
||||
deletingItem: $deletingItem,
|
||||
composeState: $composeState,
|
||||
showDeleteMessage: $showDeleteMessage
|
||||
)
|
||||
.environmentObject(chat)
|
||||
}
|
||||
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var ci: ChatItem
|
||||
var showMember: Bool = false
|
||||
var maxWidth: CGFloat
|
||||
var scrollProxy: ScrollViewProxy?
|
||||
var deleteMessage: (CIDeleteMode) -> Void
|
||||
@Binding var deletingItem: ChatItem?
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var showDeleteMessage: Bool
|
||||
|
||||
|
||||
@State private var revealed = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
|
||||
@State private var audioPlayer: AudioPlayer?
|
||||
@State private var playbackState: VoiceMessagePlaybackState = .noPlayback
|
||||
@State private var playbackTime: TimeInterval?
|
||||
@@ -513,9 +504,8 @@ struct ChatView: View {
|
||||
)
|
||||
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
chatItemReactions()
|
||||
.padding(.bottom, 4)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct ComposeState {
|
||||
var contextItem: ComposeContextItem
|
||||
var voiceMessageRecordingState: VoiceMessageRecordingState
|
||||
var inProgress = false
|
||||
var disabled = false
|
||||
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
|
||||
init(
|
||||
@@ -240,7 +241,6 @@ struct ComposeView: View {
|
||||
@State var pendingLinkUrl: URL? = nil
|
||||
@State var cancelledLinks: Set<String> = []
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var showChooseSource = false
|
||||
@State private var showMediaPicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@@ -255,8 +255,6 @@ struct ComposeView: View {
|
||||
// this is a workaround to fire an explicit event in certain cases
|
||||
@State private var stopPlayback: Bool = false
|
||||
|
||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
contextItemView()
|
||||
@@ -311,10 +309,7 @@ struct ComposeView: View {
|
||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
|
||||
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
||||
keyboardVisible: $keyboardVisible,
|
||||
sendButtonColor: chat.chatInfo.incognito
|
||||
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
||||
: .accentColor
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(.background)
|
||||
@@ -447,15 +442,7 @@ struct ComposeView: View {
|
||||
} else if (composeState.inProgress) {
|
||||
clearCurrentDraft()
|
||||
} else if !composeState.empty {
|
||||
if case .recording = composeState.voiceMessageRecordingState {
|
||||
finishVoiceMessageRecording()
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
chatModel.filesToDelete.insert(getAppFilePath(fileName))
|
||||
}
|
||||
}
|
||||
if saveLastDraft {
|
||||
saveCurrentDraft()
|
||||
}
|
||||
saveCurrentDraft()
|
||||
} else {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
@@ -668,7 +655,10 @@ struct ComposeView: View {
|
||||
return sent
|
||||
|
||||
func sending() async {
|
||||
await MainActor.run { composeState.inProgress = true }
|
||||
await MainActor.run { composeState.disabled = true }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if composeState.disabled { composeState.inProgress = true }
|
||||
}
|
||||
}
|
||||
|
||||
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
|
||||
@@ -862,6 +852,7 @@ struct ComposeView: View {
|
||||
|
||||
private func clearState(live: Bool = false) {
|
||||
if live {
|
||||
composeState.disabled = false
|
||||
composeState.inProgress = false
|
||||
} else {
|
||||
composeState = ComposeState()
|
||||
@@ -874,6 +865,12 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
private func saveCurrentDraft() {
|
||||
if case .recording = composeState.voiceMessageRecordingState {
|
||||
finishVoiceMessageRecording()
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
chatModel.filesToDelete.insert(getAppFilePath(fileName))
|
||||
}
|
||||
}
|
||||
chatModel.draft = composeState
|
||||
chatModel.draftChatId = chat.id
|
||||
}
|
||||
|
||||
@@ -22,14 +22,13 @@ struct ContextItemView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(.secondary)
|
||||
if let sender = contextItem.memberDisplayName {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sender).font(.caption).foregroundColor(.secondary)
|
||||
msgContentView(lines: 2)
|
||||
}
|
||||
} else {
|
||||
msgContentView(lines: 3)
|
||||
}
|
||||
MsgContentView(
|
||||
text: contextItem.text,
|
||||
formattedText: contextItem.formattedText,
|
||||
sender: contextItem.memberDisplayName
|
||||
)
|
||||
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
|
||||
.lineLimit(3)
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
@@ -45,15 +44,6 @@ struct ContextItemView: View {
|
||||
.background(chatItemFrameColor(contextItem, colorScheme))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
private func msgContentView(lines: Int) -> some View {
|
||||
MsgContentView(
|
||||
text: contextItem.text,
|
||||
formattedText: contextItem.formattedText
|
||||
)
|
||||
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
|
||||
.lineLimit(lines)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -28,7 +28,6 @@ struct SendMessageView: View {
|
||||
@State private var holdingVMR = false
|
||||
@Namespace var namespace
|
||||
@Binding var keyboardVisible: Bool
|
||||
var sendButtonColor = Color.accentColor
|
||||
@State private var teHeight: CGFloat = 42
|
||||
@State private var teFont: Font = .body
|
||||
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
@@ -37,7 +36,6 @@ struct SendMessageView: View {
|
||||
@State private var showCustomDisappearingMessageDialogue = false
|
||||
@State private var showCustomTimePicker = false
|
||||
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||
@State private var progressByTimeout = false
|
||||
var maxHeight: CGFloat = 360
|
||||
var minHeight: CGFloat = 37
|
||||
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||
@@ -83,7 +81,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if progressByTimeout {
|
||||
if composeState.inProgress {
|
||||
ProgressView()
|
||||
.scaleEffect(1.4)
|
||||
.frame(width: 31, height: 31, alignment: .center)
|
||||
@@ -104,15 +102,6 @@ struct SendMessageView: View {
|
||||
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
|
||||
.frame(height: teHeight)
|
||||
}
|
||||
.onChange(of: composeState.inProgress) { inProgress in
|
||||
if inProgress {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
progressByTimeout = composeState.inProgress
|
||||
}
|
||||
} else {
|
||||
progressByTimeout = false
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
@@ -130,7 +119,7 @@ struct SendMessageView: View {
|
||||
startVoiceMessageRecording: startVoiceMessageRecording,
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
holdingVMR: $holdingVMR,
|
||||
disabled: composeState.inProgress
|
||||
disabled: composeState.disabled
|
||||
)
|
||||
} else {
|
||||
voiceMessageNotAllowedButton()
|
||||
@@ -170,13 +159,13 @@ struct SendMessageView: View {
|
||||
? "checkmark.circle.fill"
|
||||
: "arrow.up.circle.fill")
|
||||
.resizable()
|
||||
.foregroundColor(sendButtonColor)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: sendButtonSize, height: sendButtonSize)
|
||||
.opacity(sendButtonOpacity)
|
||||
}
|
||||
.disabled(
|
||||
!composeState.sendEnabled ||
|
||||
composeState.inProgress ||
|
||||
composeState.disabled ||
|
||||
(!voiceMessageAllowed && composeState.voicePreview) ||
|
||||
composeState.endLiveDisabled
|
||||
)
|
||||
@@ -304,7 +293,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "mic")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.disabled(composeState.disabled)
|
||||
.frame(width: 29, height: 29)
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
@@ -389,7 +378,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "stop.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.disabled(composeState.disabled)
|
||||
.frame(width: 29, height: 29)
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ struct ChatPreviewView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
|
||||
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
|
||||
var body: some View {
|
||||
let cItem = chat.chatItems.last
|
||||
return HStack(spacing: 8) {
|
||||
@@ -103,7 +101,7 @@ struct ChatPreviewView: View {
|
||||
.kerning(-2)
|
||||
}
|
||||
|
||||
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
|
||||
private func chatPreviewLayout(_ text: Text) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
text
|
||||
.lineLimit(2)
|
||||
@@ -111,8 +109,6 @@ struct ChatPreviewView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 36)
|
||||
.privacySensitive(!showChatPreviews && !draft)
|
||||
.redacted(reason: .privacy)
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
@@ -174,7 +170,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View {
|
||||
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
|
||||
chatPreviewLayout(messageDraft(draft), draft: true)
|
||||
chatPreviewLayout(messageDraft(draft))
|
||||
} else if let cItem = cItem {
|
||||
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem))
|
||||
} else {
|
||||
|
||||
@@ -22,28 +22,10 @@ struct TerminalView: View {
|
||||
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
@State private var terminalItem: TerminalItem?
|
||||
@State private var scrolled = false
|
||||
@State private var showing = false
|
||||
|
||||
var body: some View {
|
||||
if authorized {
|
||||
terminalView()
|
||||
.onAppear {
|
||||
if showing { return }
|
||||
showing = true
|
||||
Task {
|
||||
let items = await TerminalItems.shared.items()
|
||||
await MainActor.run {
|
||||
chatModel.terminalItems = items
|
||||
chatModel.showingTerminal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if terminalItem == nil {
|
||||
chatModel.showingTerminal = false
|
||||
chatModel.terminalItems = []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||
.onAppear(perform: runAuth)
|
||||
@@ -136,8 +118,9 @@ struct TerminalView: View {
|
||||
let cmd = ChatCommand.string(composeState.message)
|
||||
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
||||
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(.now, cmd, resp)
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.addTerminalItem(.cmd(.now, cmd))
|
||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.global().async {
|
||||
|
||||
@@ -13,8 +13,6 @@ struct PrivacySettings: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -72,18 +70,6 @@ struct PrivacySettings: View {
|
||||
settingsRow("network") {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
}
|
||||
settingsRow("message") {
|
||||
Toggle("Show last messages", isOn: $showChatPreviews)
|
||||
}
|
||||
settingsRow("rectangle.and.pencil.and.ellipsis") {
|
||||
Toggle("Message draft", isOn: $saveLastDraft)
|
||||
}
|
||||
.onChange(of: saveLastDraft) { saveDraft in
|
||||
if !saveDraft {
|
||||
m.draft = nil
|
||||
m.draftChatId = nil
|
||||
}
|
||||
}
|
||||
settingsRow("link") {
|
||||
Picker("SimpleX links", selection: $simplexLinkMode) {
|
||||
ForEach(SimpleXLinkMode.values) { mode in
|
||||
|
||||
@@ -30,8 +30,6 @@ let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
|
||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
|
||||
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
||||
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
|
||||
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
|
||||
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
|
||||
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
||||
@@ -67,8 +65,6 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
|
||||
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
|
||||
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
|
||||
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
|
||||
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
|
||||
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
@@ -299,10 +295,6 @@ struct SettingsView: View {
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
}
|
||||
.onDisappear {
|
||||
chatModel.showingTerminal = false
|
||||
chatModel.terminalItems = []
|
||||
}
|
||||
}
|
||||
|
||||
private func chatDatabaseRow() -> some View {
|
||||
|
||||
@@ -127,7 +127,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
|
||||
if let connEntity = ntfMsgInfo.connEntity {
|
||||
setBestAttemptNtf(
|
||||
ntfMsgInfo.ntfsEnabled
|
||||
ntfMsgInfo.user.showNotifications
|
||||
? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity))
|
||||
: .empty
|
||||
)
|
||||
@@ -401,8 +401,4 @@ struct NtfMessages {
|
||||
var connEntity: ConnectionEntity?
|
||||
var msgTs: Date?
|
||||
var ntfMessages: [NtfMsgInfo]
|
||||
|
||||
var ntfsEnabled: Bool {
|
||||
user.showNotifications && (connEntity?.ntfsEnabled ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; };
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
||||
5C0403922A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */; };
|
||||
5C0403932A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */; };
|
||||
5C0403942A7EAA41006ACFE8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C04038F2A7EAA41006ACFE8 /* libffi.a */; };
|
||||
5C0403952A7EAA41006ACFE8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0403902A7EAA41006ACFE8 /* libgmp.a */; };
|
||||
5C0403962A7EAA41006ACFE8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0403912A7EAA41006ACFE8 /* libgmpxx.a */; };
|
||||
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
||||
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; };
|
||||
@@ -161,11 +166,6 @@
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
|
||||
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
|
||||
6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */; };
|
||||
6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF762A8F4448003B2EAF /* libgmp.a */; };
|
||||
6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF772A8F4448003B2EAF /* libgmpxx.a */; };
|
||||
6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF782A8F4448003B2EAF /* libffi.a */; };
|
||||
6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */; };
|
||||
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
||||
@@ -263,6 +263,11 @@
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; };
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
|
||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||
5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a"; sourceTree = "<group>"; };
|
||||
5C04038F2A7EAA41006ACFE8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C0403902A7EAA41006ACFE8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C0403912A7EAA41006ACFE8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
|
||||
@@ -438,11 +443,6 @@
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
|
||||
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
|
||||
6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
6462EF762A8F4448003B2EAF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
6462EF772A8F4448003B2EAF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
6462EF782A8F4448003B2EAF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a"; sourceTree = "<group>"; };
|
||||
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
||||
@@ -501,13 +501,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C0403932A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a in Frameworks */,
|
||||
5C0403922A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */,
|
||||
6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */,
|
||||
6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */,
|
||||
6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */,
|
||||
6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */,
|
||||
5C0403942A7EAA41006ACFE8 /* libffi.a in Frameworks */,
|
||||
5C0403952A7EAA41006ACFE8 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C0403962A7EAA41006ACFE8 /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -568,11 +568,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6462EF782A8F4448003B2EAF /* libffi.a */,
|
||||
6462EF762A8F4448003B2EAF /* libgmp.a */,
|
||||
6462EF772A8F4448003B2EAF /* libgmpxx.a */,
|
||||
6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */,
|
||||
6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */,
|
||||
5C04038F2A7EAA41006ACFE8 /* libffi.a */,
|
||||
5C0403902A7EAA41006ACFE8 /* libgmp.a */,
|
||||
5C0403912A7EAA41006ACFE8 /* libgmpxx.a */,
|
||||
5C04038D2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5-ghc8.10.7.a */,
|
||||
5C04038E2A7EAA41006ACFE8 /* libHSsimplex-chat-5.3.0.2-57EsBXX08D1H5qwhz1zMA5.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1478,7 +1478,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 168;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1520,7 +1520,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 168;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1600,7 +1600,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 168;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1632,7 +1632,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 168;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1664,7 +1664,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 167;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1688,7 +1688,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 5.3;
|
||||
MARKETING_VERSION = 5.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1710,7 +1710,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 167;
|
||||
CURRENT_PROJECT_VERSION = 164;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1734,7 +1734,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 5.3;
|
||||
MARKETING_VERSION = 5.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -1466,8 +1466,6 @@ public struct SecurityCode: Decodable, Equatable {
|
||||
|
||||
public struct UserContact: Decodable {
|
||||
public var userContactLinkId: Int64
|
||||
// public var connReqContact: String
|
||||
public var groupId: Int64?
|
||||
|
||||
public init(userContactLinkId: Int64) {
|
||||
self.userContactLinkId = userContactLinkId
|
||||
@@ -1929,16 +1927,6 @@ public enum ConnectionEntity: Decodable {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var ntfsEnabled: Bool {
|
||||
switch self {
|
||||
case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs ?? false
|
||||
case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs
|
||||
case .sndFileConnection: return false
|
||||
case .rcvFileConnection: return false
|
||||
case let .userContactConnection(userContact): return userContact.groupId == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct NtfMsgInfo: Decodable {
|
||||
@@ -2021,17 +2009,6 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public var memberConnected: GroupMember? {
|
||||
switch chatDir {
|
||||
case .groupRcv(let groupMember):
|
||||
switch content {
|
||||
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
|
||||
default: return nil
|
||||
}
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var showNtfDir: Bool {
|
||||
return !chatDir.sent
|
||||
}
|
||||
@@ -2530,20 +2507,6 @@ public enum CIContent: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var showMemberName: Bool {
|
||||
switch self {
|
||||
case .rcvMsgContent: return true
|
||||
case .rcvDeleted: return true
|
||||
case .rcvCall: return true
|
||||
case .rcvIntegrityError: return true
|
||||
case .rcvDecryptionError: return true
|
||||
case .rcvGroupInvitation: return true
|
||||
case .rcvModerated: return true
|
||||
case .invalidJSON: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgDecryptError: String, Decodable {
|
||||
|
||||
@@ -8,12 +8,13 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(33)
|
||||
namespace = "chat.simplex.app"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "chat.simplex.app"
|
||||
minSdkVersion(26)
|
||||
targetSdkVersion(33)
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
// !!!
|
||||
// skip version code after release to F-Droid, as it uses two version codes
|
||||
versionCode = (extra["android.version_code"] as String).toInt()
|
||||
@@ -56,8 +57,6 @@ android {
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
@@ -83,7 +82,6 @@ android {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs(
|
||||
"en",
|
||||
"bg",
|
||||
"cs",
|
||||
"de",
|
||||
"es",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="chat.simplex.app">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
|
||||
@@ -24,8 +23,9 @@
|
||||
|
||||
<application
|
||||
android:name="SimplexApp"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupOnly="false"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:backupAgent="BackupAgent"
|
||||
android:icon="@mipmap/icon"
|
||||
android:label="${app_name}"
|
||||
android:extractNativeLibs="${extract_native_libs}"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.FullBackupDataOutput
|
||||
import android.content.Context
|
||||
import chat.simplex.common.model.AppPreferences
|
||||
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_PRIVACY_FULL_BACKUP
|
||||
|
||||
class BackupAgent: BackupAgentHelper() {
|
||||
override fun onFullBackup(data: FullBackupDataOutput?) {
|
||||
if (applicationContext
|
||||
.getSharedPreferences(AppPreferences.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
.getBoolean(SHARED_PREFS_PRIVACY_FULL_BACKUP, true)
|
||||
) {
|
||||
super.onFullBackup(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import org.gradle.initialization.Environment.Properties
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
buildscript {
|
||||
val prop = java.util.Properties().apply {
|
||||
@@ -33,7 +31,6 @@ buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:${rootProject.extra["gradle.plugin.version"]}")
|
||||
classpath(kotlin("gradle-plugin", version = rootProject.extra["kotlin.version"] as String))
|
||||
classpath("org.jetbrains.kotlin:kotlin-serialization:1.3.2")
|
||||
classpath("dev.icerock.moko:resources-generator:0.22.3")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -22,7 +22,6 @@ kotlin {
|
||||
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
|
||||
optIn("androidx.compose.ui.text.ExperimentalTextApi")
|
||||
optIn("androidx.compose.material.ExperimentalMaterialApi")
|
||||
optIn("com.arkivanov.decompose.ExperimentalDecomposeApi")
|
||||
optIn("kotlinx.serialization.InternalSerializationApi")
|
||||
optIn("kotlinx.serialization.ExperimentalSerializationApi")
|
||||
optIn("androidx.compose.ui.ExperimentalComposeUiApi")
|
||||
@@ -104,11 +103,11 @@ kotlin {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(33)
|
||||
namespace = "chat.simplex.common"
|
||||
compileSdk = 33
|
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
defaultConfig {
|
||||
minSdkVersion(26)
|
||||
targetSdkVersion(33)
|
||||
minSdk = 26
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
@@ -123,11 +122,17 @@ android {
|
||||
// No other ways to exclude a file work but it's large and should be excluded
|
||||
kotlin.sourceSets["commonMain"].resources.exclude("/MR/fonts/NotoColorEmoji-Regular.ttf")
|
||||
}
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
}
|
||||
lint {
|
||||
disable += "MissingTranslation"
|
||||
disable += "ExtraTranslation"
|
||||
}
|
||||
}
|
||||
|
||||
multiplatformResources {
|
||||
multiplatformResourcesPackage = "chat.simplex.res"
|
||||
// multiplatformResourcesClassName = "MR"
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
@@ -137,97 +142,3 @@ buildConfig {
|
||||
buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("generateMRcommonMain") {
|
||||
dependsOn("adjustFormatting")
|
||||
}
|
||||
tasks.create("adjustFormatting") {
|
||||
doLast {
|
||||
val debug = false
|
||||
val stringRegex = Regex(".*<string .*</string>.*")
|
||||
val startStringRegex = Regex("<string [^>]*>")
|
||||
val endStringRegex = Regex("</string>[ ]*")
|
||||
val endTagRegex = Regex("</")
|
||||
val anyHtmlRegex = Regex("[^>]*>.*(<|>).*</string>|[^>]*>.*(<|>).*</string>")
|
||||
val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>")
|
||||
|
||||
fun String.removeCDATA(): String =
|
||||
if (contains("<![CDATA")) {
|
||||
replace("<![CDATA[", "").replace("]]></string>", "</string>")
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun String.addCDATA(filepath: String): String {
|
||||
//return this
|
||||
if (anyHtmlRegex.matches(this)) {
|
||||
val countOfStartTag = count { it == '<' }
|
||||
val countOfEndTag = count { it == '>' }
|
||||
if (countOfStartTag != countOfEndTag || countOfStartTag != endTagRegex.findAll(this).count() * 2 || !correctHtmlRegex.matches(this)) {
|
||||
if (debug) {
|
||||
println("Wrong string:")
|
||||
println(this)
|
||||
println("in $filepath")
|
||||
println(" ")
|
||||
} else {
|
||||
throw Exception("Wrong string: $this \nin $filepath")
|
||||
}
|
||||
}
|
||||
val res = replace(startStringRegex) { it.value + "<![CDATA[" }.replace(endStringRegex) { "]]>" + it.value }
|
||||
if (debug) {
|
||||
println("Changed string:")
|
||||
println(this)
|
||||
println(res)
|
||||
println(" ")
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (debug) {
|
||||
println("Correct string:")
|
||||
println(this)
|
||||
println(" ")
|
||||
}
|
||||
return this
|
||||
}
|
||||
val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$")
|
||||
kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree.forEach { file ->
|
||||
val initialLines = ArrayList<String>()
|
||||
val finalLines = ArrayList<String>()
|
||||
file.useLines { lines ->
|
||||
val multiline = ArrayList<String>()
|
||||
lines.forEach { line ->
|
||||
initialLines.add(line)
|
||||
if (stringRegex.matches(line)) {
|
||||
finalLines.add(line.removeCDATA().addCDATA(file.absolutePath))
|
||||
} else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) {
|
||||
multiline.add(line)
|
||||
} else if (multiline.isNotEmpty() && endStringRegex.containsMatchIn(line)) {
|
||||
multiline.add(line)
|
||||
finalLines.addAll(multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n"))
|
||||
multiline.clear()
|
||||
} else if (multiline.isNotEmpty()) {
|
||||
multiline.add(line)
|
||||
} else {
|
||||
finalLines.add(line)
|
||||
}
|
||||
}
|
||||
if (multiline.isNotEmpty()) {
|
||||
throw Exception("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
if (!debug && finalLines != initialLines) {
|
||||
file.writer().use {
|
||||
finalLines.forEachIndexed { index, line ->
|
||||
it.write(line)
|
||||
if (index != finalLines.lastIndex) {
|
||||
it.write("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="chat.simplex.common">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
</manifest>
|
||||
|
||||
+5
-5
@@ -39,14 +39,14 @@ private val sharedPreferencesThemes: SharedPreferences by lazy { androidAppConte
|
||||
actual val settings: Settings by lazy { SharedPreferencesSettings(sharedPreferences) }
|
||||
actual val settingsThemes: Settings by lazy { SharedPreferencesSettings(sharedPreferencesThemes) }
|
||||
|
||||
actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT -> WindowOrientation.PORTRAIT
|
||||
Configuration.ORIENTATION_LANDSCAPE -> WindowOrientation.LANDSCAPE
|
||||
else -> WindowOrientation.UNDEFINED
|
||||
actual fun screenOrientation(): ScreenOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT -> ScreenOrientation.PORTRAIT
|
||||
Configuration.ORIENTATION_LANDSCAPE -> ScreenOrientation.LANDSCAPE
|
||||
else -> ScreenOrientation.UNDEFINED
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
ModalManager.fullscreen.showInView()
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-26
@@ -80,7 +80,6 @@ object ChatModel {
|
||||
}
|
||||
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
|
||||
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
|
||||
|
||||
// current WebRTC call
|
||||
val callManager = CallManager(this)
|
||||
@@ -1386,18 +1385,6 @@ data class ChatItem (
|
||||
else -> false
|
||||
}
|
||||
|
||||
val memberConnected: GroupMember? get() =
|
||||
when (chatDir) {
|
||||
is CIDirection.GroupRcv -> when (content) {
|
||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberConnected -> chatDir.groupMember
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
|
||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
@@ -1851,19 +1838,6 @@ sealed class CIContent: ItemContent {
|
||||
is InvalidJSON -> "invalid data"
|
||||
}
|
||||
|
||||
val showMemberName: Boolean get() =
|
||||
when (this) {
|
||||
is RcvMsgContent -> true
|
||||
is RcvDeleted -> true
|
||||
is RcvCall -> true
|
||||
is RcvIntegrityError -> true
|
||||
is RcvDecryptionError -> true
|
||||
is RcvGroupInvitation -> true
|
||||
is RcvModerated -> true
|
||||
is InvalidJSON -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
||||
if (feature.hasParam) {
|
||||
|
||||
+21
-7
@@ -91,9 +91,8 @@ class AppPreferences {
|
||||
},
|
||||
set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) }
|
||||
)
|
||||
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
|
||||
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
|
||||
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
||||
val privacyFullBackup = mkBoolPreference(SHARED_PREFS_PRIVACY_FULL_BACKUP, false)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -246,8 +245,6 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
|
||||
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
|
||||
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
|
||||
private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews"
|
||||
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
|
||||
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
@@ -404,15 +401,19 @@ object ChatController {
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
if (cmd !is CC.ApiParseMarkdown) {
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
}
|
||||
val json = chatSendCmd(ctrl, c)
|
||||
val r = APIResponse.decodeStr(json)
|
||||
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
|
||||
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
||||
Log.d(TAG, "sendCmd response json $json")
|
||||
}
|
||||
chatModel.addTerminalItem(TerminalItem.resp(r.resp))
|
||||
if (r.resp !is CR.ParsedMarkdown) {
|
||||
chatModel.addTerminalItem(TerminalItem.resp(r.resp))
|
||||
}
|
||||
r.resp
|
||||
}
|
||||
}
|
||||
@@ -1289,6 +1290,13 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiParseMarkdown(text: String): List<FormattedText>? {
|
||||
val r = sendCmd(CC.ApiParseMarkdown(text))
|
||||
if (r is CR.ParsedMarkdown) return r.formattedText
|
||||
Log.e(TAG, "apiParseMarkdown bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun networkErrorAlert(r: CR): Boolean {
|
||||
return when {
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||
@@ -1849,6 +1857,7 @@ sealed class CC {
|
||||
class ApiListContacts(val userId: Long): CC()
|
||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
||||
class ApiParseMarkdown(val text: String): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||
class ApiCreateMyAddress(val userId: Long): CC()
|
||||
@@ -1954,6 +1963,7 @@ sealed class CC {
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||
is ApiParseMarkdown -> "/_parse $text"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||
is ApiCreateMyAddress -> "/_address $userId"
|
||||
@@ -2048,6 +2058,7 @@ sealed class CC {
|
||||
is ApiListContacts -> "apiListContacts"
|
||||
is ApiUpdateProfile -> "apiUpdateProfile"
|
||||
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
||||
is ApiParseMarkdown -> "apiParseMarkdown"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||
is ApiCreateMyAddress -> "apiCreateMyAddress"
|
||||
@@ -3329,6 +3340,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR()
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR()
|
||||
@@ -3453,6 +3465,7 @@ sealed class CR {
|
||||
is NewContactConnection -> "newContactConnection"
|
||||
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
||||
is VersionInfo -> "versionInfo"
|
||||
is ParsedMarkdown -> "apiParsedMarkdown"
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
@@ -3505,6 +3518,7 @@ sealed class CR {
|
||||
is ContactAliasUpdated -> withUser(user, json.encodeToString(toContact))
|
||||
is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||
is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}")
|
||||
is ParsedMarkdown -> json.encodeToString(formattedText)
|
||||
is UserContactLink -> withUser(user, contactLink.responseDetails)
|
||||
is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails)
|
||||
is UserContactLinkCreated -> withUser(user, connReqContact)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ interface RecorderInterface {
|
||||
fun stop(): Int
|
||||
}
|
||||
|
||||
expect class RecorderNative(): RecorderInterface
|
||||
expect class RecorderNative: RecorderInterface
|
||||
|
||||
interface AudioPlayerInterface {
|
||||
fun play(
|
||||
|
||||
+3
-3
@@ -19,14 +19,14 @@ expect fun isInNightMode(): Boolean
|
||||
expect val settings: Settings
|
||||
expect val settingsThemes: Settings
|
||||
|
||||
enum class WindowOrientation {
|
||||
enum class ScreenOrientation {
|
||||
UNDEFINED, PORTRAIT, LANDSCAPE
|
||||
}
|
||||
|
||||
expect fun windowOrientation(): WindowOrientation
|
||||
expect fun screenOrientation(): ScreenOrientation
|
||||
|
||||
@Composable
|
||||
expect fun windowWidth(): Dp
|
||||
expect fun screenWidth(): Dp
|
||||
|
||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||
|
||||
|
||||
+11
-26
@@ -38,7 +38,6 @@ import chat.simplex.common.views.chatlist.updateChatSettings
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -52,16 +51,15 @@ fun ChatInfoView(
|
||||
close: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val contact = rememberUpdatedState(contact).value
|
||||
val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } }
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && currentUser != null) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
|
||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||
}
|
||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
@@ -205,10 +203,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -244,7 +239,7 @@ fun ChatInfoLayout(
|
||||
currentUser: User,
|
||||
sendReceipts: State<SendReceipts>,
|
||||
setSendReceipts: (SendReceipts) -> Unit,
|
||||
connStats: State<ConnectionStats?>,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
@@ -261,15 +256,10 @@ fun ChatInfoLayout(
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
KeyChangeEffect(chat.id) {
|
||||
scope.launch { scrollState.scrollTo(0) }
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -410,7 +400,6 @@ fun LocalAliasEditor(
|
||||
updateValue: (String) -> Unit
|
||||
) {
|
||||
var value by rememberSaveable { mutableStateOf(initialValue) }
|
||||
var updatedValueAtLeastOnce = remember { false }
|
||||
val modifier = if (center)
|
||||
Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).widthIn(min = 100.dp)
|
||||
else
|
||||
@@ -435,23 +424,19 @@ fun LocalAliasEditor(
|
||||
keyboardActions = KeyboardActions(onDone = { updateValue(value) })
|
||||
) {
|
||||
value = it
|
||||
updatedValueAtLeastOnce = true
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
var prevValue = value
|
||||
snapshotFlow { value }
|
||||
.distinctUntilChanged()
|
||||
.onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing
|
||||
.conflate() // get the latest value
|
||||
.filter { it == value && it != prevValue } // don't process old ones
|
||||
.filter { it == value } // don't process old ones
|
||||
.collect {
|
||||
updateValue(it)
|
||||
prevValue = it
|
||||
updateValue(value)
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { if (updatedValueAtLeastOnce) updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
|
||||
onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-118
@@ -17,11 +17,11 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -66,11 +66,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect { chatId ->
|
||||
if (activeChat.value?.id != chatId && chatId != null) {
|
||||
.collect {
|
||||
if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) {
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
// Also for situation when chatId changes after clicking in notification, etc
|
||||
activeChat.value = chatModel.getChat(chatId)
|
||||
activeChat.value = chatModel.getChat(chatModel.chatId.value!!)
|
||||
}
|
||||
markUnreadChatAsRead(activeChat, chatModel)
|
||||
}
|
||||
@@ -98,14 +98,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
val view = LocalMultiplatformView()
|
||||
if (activeChat.value == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
} else {
|
||||
val chat = activeChat.value!!
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
val unreadCount = remember {
|
||||
derivedStateOf {
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@@ -134,45 +133,27 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
chatModel.chatId.value = null
|
||||
},
|
||||
info = {
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
ModalManager.end.closeModals()
|
||||
return@ChatLayout
|
||||
}
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||
var preloadedContactInfo: Pair<ConnectionStats, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
}
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
var groupLink = link?.first
|
||||
var groupLinkMemberRole = link?.second
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
|
||||
groupLink = it.first;
|
||||
groupLinkMemberRole = it.second
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
@@ -718,7 +699,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
)
|
||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem ->
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem ->
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
@@ -760,73 +741,27 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
val nextItem = if (i - 1 >= 0) reversedChatItems[i - 1] else null
|
||||
fun getConnectedMemberNames(): List<String> {
|
||||
val ns = mutableListOf<String>()
|
||||
var idx = i
|
||||
while (idx < reversedChatItems.size) {
|
||||
val m = reversedChatItems[idx].memberConnected
|
||||
if (m != null) {
|
||||
ns.add(m.displayName)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
idx++
|
||||
}
|
||||
return ns
|
||||
}
|
||||
if (cItem.memberConnected != null && nextItem?.memberConnected != null) {
|
||||
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
|
||||
Box(Modifier.size(0.dp)) {}
|
||||
} else {
|
||||
val member = cItem.chatDir.groupMember
|
||||
if (showMemberImage(member, prevItem)) {
|
||||
Column(
|
||||
val member = cItem.chatDir.groupMember
|
||||
val showMember = showMemberImage(member, prevItem)
|
||||
Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) {
|
||||
if (showMember) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
if (cItem.content.showMemberName) {
|
||||
Text(
|
||||
member.displayName,
|
||||
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
swipeableModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
showMemberInfo(chat.chatInfo.groupInfo, member)
|
||||
}
|
||||
) {
|
||||
MemberImage(member)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
showMemberInfo(chat.chatInfo.groupInfo, member)
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
MemberImage(member)
|
||||
}
|
||||
Spacer(Modifier.size(4.dp))
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
}
|
||||
@@ -842,7 +777,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) {
|
||||
if (cItem.isRcvNew) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
delay(600)
|
||||
@@ -974,19 +909,17 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
onLongClick = { showDropDown.value = true }
|
||||
)
|
||||
|
||||
Box {
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
onClick = {
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
)
|
||||
showDropDown.value = false
|
||||
})
|
||||
}
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
onClick = {
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
)
|
||||
showDropDown.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,11 +954,9 @@ fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean {
|
||||
(prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId)
|
||||
}
|
||||
|
||||
val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image)
|
||||
ProfileImage(38.dp, member.memberProfile.image)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+17
-38
@@ -16,8 +16,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.Indigo
|
||||
import chat.simplex.common.ui.theme.isSystemInDarkTheme
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -114,7 +112,7 @@ data class ComposeState(
|
||||
}
|
||||
|
||||
val empty: Boolean
|
||||
get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
|
||||
get() = message.isEmpty() && preview is ComposePreview.NoPreview
|
||||
|
||||
companion object {
|
||||
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
|
||||
@@ -232,7 +230,6 @@ fun ComposeView(
|
||||
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get()
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
|
||||
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
|
||||
@@ -243,7 +240,7 @@ fun ComposeView(
|
||||
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
|
||||
|
||||
fun parseMessage(msg: String): String? {
|
||||
val parsedMsg = parseToMarkdown(msg)
|
||||
val parsedMsg = runBlocking { chatModel.controller.apiParseMarkdown(msg) }
|
||||
val link = parsedMsg?.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }
|
||||
return link?.text
|
||||
}
|
||||
@@ -286,20 +283,6 @@ fun ComposeView(
|
||||
cancelledLinks.clear()
|
||||
}
|
||||
|
||||
fun clearPrevDraft(prevChatId: String?) {
|
||||
if (chatModel.draftChatId.value == prevChatId) {
|
||||
chatModel.draft.value = null
|
||||
chatModel.draftChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCurrentDraft() {
|
||||
if (chatModel.draftChatId.value == chat.id) {
|
||||
chatModel.draft.value = null
|
||||
chatModel.draftChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun clearState(live: Boolean = false) {
|
||||
if (live) {
|
||||
composeState.value = composeState.value.copy(inProgress = false)
|
||||
@@ -395,7 +378,6 @@ fun ComposeView(
|
||||
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
|
||||
sending()
|
||||
}
|
||||
clearCurrentDraft()
|
||||
|
||||
if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
@@ -723,6 +705,13 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCurrentDraft() {
|
||||
if (chatModel.draftChatId.value == chat.id) {
|
||||
chatModel.draft.value = null
|
||||
chatModel.draftChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
|
||||
if (!chat.userCanSend) {
|
||||
clearCurrentDraft()
|
||||
@@ -730,38 +719,29 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
|
||||
DisposableEffectOnGone {
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearPrevDraft(prevChatId)
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
} else if (cs.inProgress) {
|
||||
clearPrevDraft(prevChatId)
|
||||
} else if (!cs.empty) {
|
||||
} else if (composeState.value.inProgress) {
|
||||
clearCurrentDraft()
|
||||
} else if (!composeState.value.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
if (saveLastDraft) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = prevChatId
|
||||
}
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
|
||||
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
} else {
|
||||
clearPrevDraft(prevChatId)
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
}
|
||||
|
||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||
val sendButtonColor =
|
||||
if (chat.chatInfo.incognito)
|
||||
if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
|
||||
else MaterialTheme.colors.primary
|
||||
SendMsgView(
|
||||
composeState,
|
||||
showVoiceRecordIcon = true,
|
||||
@@ -773,7 +753,6 @@ fun ComposeView(
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = userIsObserver.value,
|
||||
userCanSend = userCanSend.value,
|
||||
sendButtonColor = sendButtonColor,
|
||||
timedMessageAllowed = timedMessageAllowed,
|
||||
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
|
||||
sendMessage = { ttl ->
|
||||
|
||||
+6
-28
@@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.model.*
|
||||
@@ -29,17 +27,6 @@ fun ContextItemView(
|
||||
val sent = contextItem.chatDir.sent
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
|
||||
@Composable
|
||||
fun msgContentView(lines: Int) {
|
||||
MarkdownText(
|
||||
contextItem.text, contextItem.formattedText,
|
||||
maxLines = lines,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
@@ -62,21 +49,12 @@ fun ContextItemView(
|
||||
contentDescription = stringResource(MR.strings.icon_descr_context),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
val sender = contextItem.memberDisplayName
|
||||
if (sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
msgContentView(lines = 2)
|
||||
}
|
||||
} else {
|
||||
msgContentView(lines = 3)
|
||||
}
|
||||
MarkdownText(
|
||||
contextItem.text, contextItem.formattedText,
|
||||
sender = contextItem.memberDisplayName, senderBold = true, maxLines = 3,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = cancelContextItem) {
|
||||
Icon(
|
||||
|
||||
+6
-18
@@ -41,7 +41,6 @@ fun SendMsgView(
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
userIsObserver: Boolean,
|
||||
userCanSend: Boolean,
|
||||
sendButtonColor: Color = MaterialTheme.colors.primary,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
timedMessageAllowed: Boolean = false,
|
||||
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
|
||||
@@ -65,22 +64,12 @@ fun SendMsgView(
|
||||
|
||||
Box(Modifier.padding(vertical = 8.dp)) {
|
||||
val cs = composeState.value
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(composeState.value.inProgress) {
|
||||
progressByTimeout = if (composeState.value.inProgress) {
|
||||
delay(500)
|
||||
composeState.value.inProgress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
|
||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
|
||||
if (!cs.inProgress) {
|
||||
sendMessage(null)
|
||||
}
|
||||
sendMessage(null)
|
||||
}
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
@@ -109,7 +98,7 @@ fun SendMsgView(
|
||||
}
|
||||
}
|
||||
when {
|
||||
progressByTimeout -> ProgressIndicator()
|
||||
showProgress -> ProgressIndicator()
|
||||
showVoiceButton -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||
@@ -195,12 +184,12 @@ fun SendMsgView(
|
||||
|
||||
val menuItems = MenuItems()
|
||||
if (menuItems.isNotEmpty()) {
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) { showDropdown.value = true }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
|
||||
DefaultDropdownMenu(showDropdown) {
|
||||
menuItems.forEach { composable -> composable() }
|
||||
}
|
||||
} else {
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage)
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,7 +439,6 @@ private fun SendMsgButton(
|
||||
icon: Painter,
|
||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||
alpha: Animatable<Float, AnimationVector1D>,
|
||||
sendButtonColor: Color,
|
||||
enabled: Boolean,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
@@ -478,7 +466,7 @@ private fun SendMsgButton(
|
||||
.padding(4.dp)
|
||||
.alpha(alpha.value)
|
||||
.clip(CircleShape)
|
||||
.background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary)
|
||||
.background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)
|
||||
.padding(3.dp)
|
||||
)
|
||||
}
|
||||
|
||||
+8
-5
@@ -71,9 +71,6 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
|
||||
removeContact = { contactId -> selectedContacts.removeIf { it == contactId } },
|
||||
close = close,
|
||||
)
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
@@ -176,7 +173,7 @@ fun AddGroupMembersLayout(
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionView(stringResource(MR.strings.select_contacts)) {
|
||||
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText)
|
||||
SearchRowView(searchText, selectedContacts.size)
|
||||
}
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
|
||||
}
|
||||
@@ -187,7 +184,8 @@ fun AddGroupMembersLayout(
|
||||
|
||||
@Composable
|
||||
private fun SearchRowView(
|
||||
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) },
|
||||
selectedContactsSize: Int
|
||||
) {
|
||||
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
|
||||
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
|
||||
@@ -196,6 +194,11 @@ private fun SearchRowView(
|
||||
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(selectedContactsSize) {
|
||||
searchText.value = searchText.value.copy("")
|
||||
hideKeyboard(view)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+76
-81
@@ -8,8 +8,8 @@ import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -33,12 +33,11 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
@@ -133,10 +132,7 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -181,92 +177,79 @@ fun GroupChatInfoLayout(
|
||||
leaveGroup: () -> Unit,
|
||||
manageGroupLink: () -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
KeyChangeEffect(chat.id) {
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
|
||||
LazyColumn(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
state = listState
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
GroupChatInfoHeader(chat.chatInfo)
|
||||
}
|
||||
SectionSpacer()
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
GroupChatInfoHeader(chat.chatInfo)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
}
|
||||
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
GroupPreferencesButton(openPreferences)
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
}
|
||||
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
GroupPreferencesButton(openPreferences)
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
GroupLinkButton(manageGroupLink)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
GroupLinkButton(manageGroupLink)
|
||||
}
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
AddMembersButton(tint, onAddMembersClick)
|
||||
}
|
||||
if (members.size > 8) {
|
||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
}
|
||||
SectionItemView(minHeight = 54.dp) {
|
||||
MemberRow(groupInfo.membership, user = true)
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
AddMembersButton(tint, onAddMembersClick)
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
|
||||
if (members.size > 8) {
|
||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
}
|
||||
SectionItemView(minHeight = 54.dp) {
|
||||
MemberRow(groupInfo.membership, user = true)
|
||||
}
|
||||
MembersList(filteredMembers.value, showMemberInfo)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupButton(deleteGroup)
|
||||
}
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupButton(leaveGroup)
|
||||
}
|
||||
}
|
||||
items(filteredMembers.value) { member ->
|
||||
Divider()
|
||||
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
|
||||
MemberRow(member)
|
||||
}
|
||||
}
|
||||
item {
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupButton(deleteGroup)
|
||||
}
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupButton(leaveGroup)
|
||||
}
|
||||
}
|
||||
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
|
||||
}
|
||||
if (developerTools) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
|
||||
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +330,18 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
|
||||
Column {
|
||||
members.forEachIndexed { index, member ->
|
||||
Divider()
|
||||
SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) {
|
||||
MemberRow(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
Row(
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit) {
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
|
||||
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
||||
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
|
||||
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -34,7 +34,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
if (link != null) {
|
||||
groupLink = link.first
|
||||
groupLinkMemberRole.value = link.second
|
||||
onGroupLinkUpdated(link)
|
||||
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
|
||||
}
|
||||
creatingLink = false
|
||||
}
|
||||
@@ -60,7 +60,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
if (link != null) {
|
||||
groupLink = link.first
|
||||
groupLinkMemberRole.value = link.second
|
||||
onGroupLinkUpdated(link)
|
||||
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
if (r) {
|
||||
groupLink = null
|
||||
onGroupLinkUpdated(null)
|
||||
onGroupLinkUpdated(null to null)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -8,19 +8,43 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CIEventView(text: AnnotatedString) {
|
||||
fun CIEventView(ci: ChatItem) {
|
||||
@Composable
|
||||
fun chatEventTextView(text: AnnotatedString) {
|
||||
Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
|
||||
}
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
|
||||
val memberDisplayName = ci.memberDisplayName
|
||||
if (memberDisplayName != null) {
|
||||
chatEventTextView(
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
}.plus(chatEventText(ci))
|
||||
)
|
||||
} else {
|
||||
chatEventTextView(chatEventText(ci))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
|
||||
|
||||
fun chatEventText(ci: ChatItem): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) }
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
@@ -28,6 +52,8 @@ fun CIEventView(text: AnnotatedString) {
|
||||
@Composable
|
||||
fun CIEventViewPreview() {
|
||||
SimpleXTheme {
|
||||
CIEventView(buildAnnotatedString { append("event happened") })
|
||||
CIEventView(
|
||||
ChatItem.getGroupEventSample()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -32,6 +32,7 @@ fun CIRcvDecryptionError(
|
||||
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
|
||||
findModelChat: (String) -> Chat?,
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
showMember: Boolean
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
@@ -45,6 +46,7 @@ fun CIRcvDecryptionError(
|
||||
fun BasicDecryptionErrorItem() {
|
||||
DecryptionErrorItem(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.decryption_error),
|
||||
@@ -62,6 +64,7 @@ fun CIRcvDecryptionError(
|
||||
if (modelContactStats.ratchetSyncAllowed) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.fix_connection_question),
|
||||
@@ -75,6 +78,7 @@ fun CIRcvDecryptionError(
|
||||
} else if (!modelContactStats.ratchetSyncSupported) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.fix_connection_not_supported_by_contact),
|
||||
@@ -99,6 +103,7 @@ fun CIRcvDecryptionError(
|
||||
if (modelMemberStats.ratchetSyncAllowed) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.fix_connection_question),
|
||||
@@ -112,6 +117,7 @@ fun CIRcvDecryptionError(
|
||||
} else if (!modelMemberStats.ratchetSyncSupported) {
|
||||
DecryptionErrorItemFixButton(
|
||||
ci,
|
||||
showMember,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member),
|
||||
@@ -134,6 +140,7 @@ fun CIRcvDecryptionError(
|
||||
@Composable
|
||||
fun DecryptionErrorItemFixButton(
|
||||
ci: ChatItem,
|
||||
showMember: Boolean,
|
||||
onClick: () -> Unit,
|
||||
syncSupported: Boolean
|
||||
) {
|
||||
@@ -152,6 +159,7 @@ fun DecryptionErrorItemFixButton(
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
@@ -181,6 +189,7 @@ fun DecryptionErrorItemFixButton(
|
||||
@Composable
|
||||
fun DecryptionErrorItem(
|
||||
ci: ChatItem,
|
||||
showMember: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
@@ -195,6 +204,7 @@ fun DecryptionErrorItem(
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
|
||||
},
|
||||
|
||||
+13
-66
@@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -29,22 +29,13 @@ import kotlinx.datetime.Clock
|
||||
|
||||
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
|
||||
|
||||
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
|
||||
|
||||
fun chatEventText(ci: ChatItem): AnnotatedString =
|
||||
chatEventText(ci.content.text, ci.timestampText)
|
||||
|
||||
fun chatEventText(eventText: String, ts: String): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append("$eventText $ts") }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(
|
||||
cInfo: ChatInfo,
|
||||
cItem: ChatItem,
|
||||
composeState: MutableState<ComposeState>,
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
showMember: Boolean = false,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
@@ -62,7 +53,6 @@ fun ChatItemView(
|
||||
findModelMember: (String) -> GroupMember?,
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
getConnectedMemberNames: (() -> List<String>)? = null,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
@@ -105,8 +95,7 @@ fun ChatItemView(
|
||||
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
||||
if (r.totalReacted > 1) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
"${r.totalReacted}",
|
||||
Text("${r.totalReacted}",
|
||||
fontSize = 11.5.sp,
|
||||
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
@@ -127,7 +116,7 @@ fun ChatItemView(
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
@@ -257,7 +246,7 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
@@ -276,7 +265,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
@@ -287,48 +276,9 @@ fun ChatItemView(
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
fun eventItemViewText(): AnnotatedString {
|
||||
val memberDisplayName = cItem.memberDisplayName
|
||||
return if (memberDisplayName != null) {
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
}.plus(chatEventText(cItem))
|
||||
} else {
|
||||
chatEventText(cItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun EventItemView() {
|
||||
CIEventView(eventItemViewText())
|
||||
}
|
||||
|
||||
fun membersConnectedText(): String? {
|
||||
return if (getConnectedMemberNames != null) {
|
||||
val ns = getConnectedMemberNames()
|
||||
when {
|
||||
ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2)
|
||||
ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2])
|
||||
ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1])
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun membersConnectedItemText(): AnnotatedString {
|
||||
val t = membersConnectedText()
|
||||
return if (t != null) {
|
||||
chatEventText(t, cItem.timestampText)
|
||||
} else {
|
||||
eventItemViewText()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModeratedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage)
|
||||
@@ -342,17 +292,14 @@ fun ChatItemView(
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
|
||||
else -> EventItemView()
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> EventItemView()
|
||||
is CIContent.RcvConnEventContent -> EventItemView()
|
||||
is CIContent.SndConnEventContent -> EventItemView()
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
val sent = ci.chatDir.sent
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
@@ -30,6 +30,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
|
||||
+15
-37
@@ -20,11 +20,9 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.base64ToBitmap
|
||||
import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE
|
||||
import chat.simplex.res.MR
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -34,6 +32,7 @@ fun FramedItemView(
|
||||
ci: ChatItem,
|
||||
uriHandler: UriHandler? = null,
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
showMember: Boolean = false,
|
||||
linkMode: SimplexLinkMode,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -50,39 +49,17 @@ fun FramedItemView(
|
||||
@Composable
|
||||
fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f)
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
||||
MarkdownText(
|
||||
qi.text,
|
||||
qi.formattedText,
|
||||
maxLines = lines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
|
||||
linkMode = linkMode,
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgView(qi: CIQuote) {
|
||||
Box(
|
||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
val sender = qi.sender(membership())
|
||||
if (sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
ciQuotedMsgTextView(qi, lines = 2)
|
||||
}
|
||||
} else {
|
||||
ciQuotedMsgTextView(qi, lines = 3)
|
||||
}
|
||||
MarkdownText(
|
||||
qi.text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3,
|
||||
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
|
||||
linkMode = linkMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +156,7 @@ fun FramedItemView(
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +207,7 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
@@ -238,29 +215,29 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
}
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick)
|
||||
else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +253,7 @@ fun FramedItemView(
|
||||
fun CIMarkdownText(
|
||||
ci: ChatItem,
|
||||
chatTTL: Int?,
|
||||
showMember: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
@@ -283,7 +261,7 @@ fun CIMarkdownText(
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText,
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
|
||||
)
|
||||
|
||||
+4
-3
@@ -24,8 +24,8 @@ import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, timedMessagesTTL) {
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
CIMsgError(ci, timedMessagesTTL, showMember) {
|
||||
when (msgError) {
|
||||
is MsgErrorType.MsgSkipped ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -50,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
@@ -63,6 +63,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
|
||||
+2
-1
@@ -20,7 +20,7 @@ import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Surface(
|
||||
@@ -47,6 +47,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
private fun MarkedDeletedText(text: String) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
|
||||
+15
-27
@@ -1,32 +1,39 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.*
|
||||
|
||||
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
|
||||
val boldFont = SpanStyle(fontWeight = FontWeight.Medium)
|
||||
|
||||
fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMemberBold: Boolean) {
|
||||
if (chatItem.chatDir is CIDirection.GroupRcv) {
|
||||
val name = chatItem.chatDir.groupMember.memberProfile.displayName
|
||||
if (groupMemberBold) b.withStyle(boldFont) { append(name) }
|
||||
else b.append(name)
|
||||
b.append(": ")
|
||||
}
|
||||
}
|
||||
|
||||
fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolean) {
|
||||
if (sender != null) {
|
||||
if (senderBold) b.withStyle(boldFont) { append(sender) }
|
||||
@@ -158,8 +165,7 @@ fun MarkdownText (
|
||||
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
if (hasLinks && uriHandler != null) {
|
||||
val icon = remember { mutableStateOf(PointerIcon.Default) }
|
||||
ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow,
|
||||
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
|
||||
onLongClick = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) }
|
||||
@@ -182,15 +188,6 @@ fun MarkdownText (
|
||||
uriHandler.openVerifiedSimplexUri(annotation.item)
|
||||
}
|
||||
},
|
||||
onHover = { offset ->
|
||||
icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let {
|
||||
PointerIcon.Hand
|
||||
} ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let {
|
||||
PointerIcon.Hand
|
||||
} ?: PointerIcon.Default
|
||||
},
|
||||
shouldConsumeEvent = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any()
|
||||
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any()
|
||||
@@ -214,7 +211,6 @@ fun ClickableText(
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
onClick: (Int) -> Unit,
|
||||
onLongClick: (Int) -> Unit = {},
|
||||
onHover: (Int) -> Unit = {},
|
||||
shouldConsumeEvent: (Int) -> Boolean
|
||||
) {
|
||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
@@ -238,14 +234,6 @@ fun ClickableText(
|
||||
consume
|
||||
}
|
||||
)
|
||||
}.pointerInput(onHover) {
|
||||
if (appPlatform.isDesktop) {
|
||||
detectCursorMove { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
onHover(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicText(
|
||||
|
||||
+7
-21
@@ -44,12 +44,11 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
|
||||
click = { directChatAction(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -58,7 +57,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -104,7 +103,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
|
||||
fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
if (chatInfo.ready) {
|
||||
withBGApi { openChat(chatInfo, chatModel) }
|
||||
withApi { openChat(chatInfo, chatModel) }
|
||||
} else {
|
||||
pendingContactAlertDialog(chatInfo, chatModel)
|
||||
}
|
||||
@@ -114,7 +113,7 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel)
|
||||
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
|
||||
else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) }
|
||||
else -> withApi { openChat(ChatInfo.Group(groupInfo), chatModel) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,12 +368,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
stringResource(MR.strings.delete_verb),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {
|
||||
if (chatModel.chatId.value == null) {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
}
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@@ -523,10 +517,7 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -559,10 +550,7 @@ fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(groupInfo.id)
|
||||
if (chatModel.chatId.value == groupInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
ntfManager.cancelNotificationsForChat(groupInfo.id)
|
||||
}
|
||||
}
|
||||
@@ -669,7 +657,6 @@ fun PreviewChatListNavLinkDirect() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -709,7 +696,6 @@ fun PreviewChatListNavLinkGroup() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
||||
-7
@@ -61,13 +61,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
connectIfOpenedViaUri(url, chatModel)
|
||||
}
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
ModalManager.end.closeModalsExceptFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
+26
-29
@@ -31,7 +31,6 @@ import dev.icerock.moko.resources.ImageResource
|
||||
@Composable
|
||||
fun ChatPreviewView(
|
||||
chat: Chat,
|
||||
showChatPreviews: Boolean,
|
||||
chatModelDraft: ComposeState?,
|
||||
chatModelDraftChatId: ChatId?,
|
||||
currentUserProfileDisplayName: String?,
|
||||
@@ -141,34 +140,32 @@ fun ChatPreviewView(
|
||||
fun chatPreviewText() {
|
||||
val ci = chat.chatItems.lastOrNull()
|
||||
if (ci != null) {
|
||||
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
|
||||
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
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
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
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
@@ -339,6 +336,6 @@ fun unreadCountStr(n: Int): String {
|
||||
@Composable
|
||||
fun PreviewChatPreviewView() {
|
||||
SimpleXTheme {
|
||||
ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
ChatPreviewView(Chat.sampleData, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@ fun UserPicker(
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
val maxWidth = with(LocalDensity.current) { screenWidth() * density }
|
||||
Box(Modifier
|
||||
.fillMaxSize()
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) }
|
||||
@@ -201,7 +201,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
|
||||
fun UserProfileRow(u: User) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
.widthIn(max = screenWidth() * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
+29
-7
@@ -78,6 +78,7 @@ fun DatabaseView(
|
||||
chatArchiveName,
|
||||
chatArchiveTime,
|
||||
chatLastStart,
|
||||
m.controller.appPrefs.privacyFullBackup,
|
||||
appFilesCountAndSize,
|
||||
chatItemTTL,
|
||||
m.currentUser.value,
|
||||
@@ -127,6 +128,7 @@ fun DatabaseLayout(
|
||||
chatArchiveName: MutableState<String?>,
|
||||
chatArchiveTime: MutableState<Instant?>,
|
||||
chatLastStart: MutableState<Instant?>,
|
||||
privacyFullBackup: SharedPreference<Boolean>,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
chatItemTTL: MutableState<ChatItemTTL>,
|
||||
currentUser: User?,
|
||||
@@ -166,13 +168,6 @@ fun DatabaseLayout(
|
||||
SectionView(stringResource(MR.strings.run_chat_section)) {
|
||||
RunChatSetting(runChat, stopped, startChat, stopChatAlert)
|
||||
}
|
||||
SectionTextFooter(
|
||||
if (stopped) {
|
||||
stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)
|
||||
} else {
|
||||
stringResource(MR.strings.stop_chat_to_enable_database_actions)
|
||||
}
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.chat_database_section)) {
|
||||
@@ -185,6 +180,8 @@ fun DatabaseLayout(
|
||||
iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
AppDataBackupPreference(privacyFullBackup, initialRandomDBPassphrase)
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_ios_share),
|
||||
stringResource(MR.strings.export_database),
|
||||
@@ -228,6 +225,13 @@ fun DatabaseLayout(
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
}
|
||||
SectionTextFooter(
|
||||
if (stopped) {
|
||||
stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)
|
||||
} else {
|
||||
stringResource(MR.strings.stop_chat_to_enable_database_actions)
|
||||
}
|
||||
)
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) {
|
||||
@@ -254,6 +258,23 @@ fun DatabaseLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppDataBackupPreference(privacyFullBackup: SharedPreference<Boolean>, initialRandomDBPassphrase: SharedPreference<Boolean>) {
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_backup),
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
pref = privacyFullBackup,
|
||||
text = stringResource(MR.strings.full_backup)
|
||||
) {
|
||||
if (initialRandomDBPassphrase.get()) {
|
||||
exportProhibitedAlert()
|
||||
privacyFullBackup.set(false)
|
||||
} else {
|
||||
privacyFullBackup.set(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setChatItemTTLAlert(
|
||||
m: ChatModel, selectedChatItemTTL: MutableState<ChatItemTTL>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
@@ -662,6 +683,7 @@ fun PreviewDatabaseLayout() {
|
||||
chatArchiveName = remember { mutableStateOf("dummy_archive") },
|
||||
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
|
||||
chatLastStart = remember { mutableStateOf(Clock.System.now()) },
|
||||
privacyFullBackup = SharedPreference({ true }, {}),
|
||||
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
|
||||
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
|
||||
currentUser = User.sampleData,
|
||||
|
||||
+15
-30
@@ -8,7 +8,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -52,31 +51,26 @@ class AlertManager {
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = ::hideAlert,
|
||||
title = {
|
||||
DefaultDialog(onDismissRequest = ::hideAlert) {
|
||||
Column(
|
||||
Modifier
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
},
|
||||
buttons = {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
buttons()
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
buttons()
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,22 +154,13 @@ class AlertManager {
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
hideAlert()
|
||||
},
|
||||
Modifier.focusRequester(focusRequester)
|
||||
) {
|
||||
Text(confirmText, color = Color.Unspecified)
|
||||
}
|
||||
TextButton(onClick = {
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = Color.Unspecified) }
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
|
||||
-11
@@ -92,17 +92,6 @@ suspend fun PointerInputScope.detectGesture(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun PointerInputScope.detectCursorMove(onMove: (Offset) -> Unit = {},) = coroutineScope {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
val event = awaitPointerEvent()
|
||||
if (event.type == PointerEventType.Move) {
|
||||
onMove(event.changes[0].position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun AwaitPointerEventScope.consumeUntilUp() {
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
|
||||
-6
@@ -97,12 +97,6 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
modalCount.value = 0
|
||||
}
|
||||
|
||||
fun closeModalsExceptFirst() {
|
||||
while (modalCount.value > 1) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun showInView() {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.windowWidth
|
||||
import chat.simplex.common.platform.screenWidth
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
@@ -238,7 +238,7 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color
|
||||
@Composable
|
||||
fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick) {
|
||||
val screenWidthDp = windowWidth()
|
||||
val screenWidthDp = screenWidth()
|
||||
Text(title)
|
||||
Text(
|
||||
value,
|
||||
|
||||
+4
-45
@@ -306,10 +306,10 @@ fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver(
|
||||
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
|
||||
DisposableEffect(Unit) {
|
||||
always()
|
||||
val orientation = windowOrientation()
|
||||
val orientation = screenOrientation()
|
||||
onDispose {
|
||||
whenDispose()
|
||||
if (orientation == windowOrientation()) {
|
||||
if (orientation == screenOrientation()) {
|
||||
whenGone()
|
||||
}
|
||||
}
|
||||
@@ -320,53 +320,12 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}
|
||||
fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) {
|
||||
DisposableEffect(Unit) {
|
||||
always()
|
||||
val orientation = windowOrientation()
|
||||
val orientation = screenOrientation()
|
||||
onDispose {
|
||||
whenDispose()
|
||||
if (orientation != windowOrientation()) {
|
||||
if (orientation != screenOrientation()) {
|
||||
whenRotate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the [block] only after initial value of the [key1] changes, not after initial launch
|
||||
* */
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun <T> KeyChangeEffect(
|
||||
key1: T?,
|
||||
block: suspend CoroutineScope.(prevKey: T?) -> Unit
|
||||
) {
|
||||
var prevKey by remember { mutableStateOf(key1) }
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1) {
|
||||
if (anyChange || key1 != prevKey) {
|
||||
block(prevKey)
|
||||
prevKey = key1
|
||||
anyChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the [block] only after initial value of the [key1] or [key2] changes, not after initial launch
|
||||
* */
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun KeyChangeEffect(
|
||||
key1: Any?,
|
||||
key2: Any?,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
val initialKey = remember { key1 }
|
||||
val initialKey2 = remember { key2 }
|
||||
var anyChange by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1) {
|
||||
if (anyChange || key1 != initialKey || key2 != initialKey2) {
|
||||
block()
|
||||
anyChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-47
@@ -6,11 +6,10 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.input.key.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.ScreenOrientation
|
||||
import chat.simplex.common.platform.screenOrientation
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.SimpleButton
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -26,43 +25,9 @@ fun PasscodeView(
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
@Composable
|
||||
fun Modifier.handleKeyboard(): Modifier {
|
||||
val numbers = remember {
|
||||
arrayOf(
|
||||
Key.Zero, Key.One, Key.Two, Key.Three, Key.Four, Key.Five, Key.Six, Key.Seven, Key.Eight, Key.Nine,
|
||||
Key.NumPad0, Key.NumPad1, Key.NumPad2, Key.NumPad3, Key.NumPad4, Key.NumPad5, Key.NumPad6, Key.NumPad7, Key.NumPad8, Key.NumPad9
|
||||
)
|
||||
}
|
||||
return onPreviewKeyEvent {
|
||||
if (it.key in numbers && it.type == KeyEventType.KeyDown) {
|
||||
if (passcode.value.length < 16) {
|
||||
passcode.value += numbers.indexOf(it.key) % 10
|
||||
}
|
||||
true
|
||||
} else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown && (it.isCtrlPressed || it.isMetaPressed)) {
|
||||
passcode.value = ""
|
||||
true
|
||||
} else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown) {
|
||||
passcode.value = passcode.value.dropLast(1)
|
||||
true
|
||||
} else if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) {
|
||||
if ((submitEnabled?.invoke(passcode.value) != false && passcode.value.length >= 4)) {
|
||||
submit()
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalLayout() {
|
||||
Column(
|
||||
Modifier.handleKeyboard().focusRequester(focusRequester),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
@@ -73,7 +38,7 @@ fun PasscodeView(
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, true)
|
||||
Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row {
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
@@ -83,9 +48,9 @@ fun PasscodeView(
|
||||
|
||||
@Composable
|
||||
fun HorizontalLayout() {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) {
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING),
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
@@ -99,7 +64,7 @@ fun PasscodeView(
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING),
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
@@ -125,14 +90,9 @@ fun PasscodeView(
|
||||
}
|
||||
}
|
||||
|
||||
if (windowOrientation() == WindowOrientation.PORTRAIT || appPlatform.isDesktop) {
|
||||
if (screenOrientation() == ScreenOrientation.PORTRAIT) {
|
||||
VerticalLayout()
|
||||
} else {
|
||||
HorizontalLayout()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
// Disallow to steal a focus by clicking on buttons or using Tab
|
||||
focusRequester.captureFocus()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.localauth
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -15,7 +16,6 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
@@ -39,7 +39,7 @@ fun PasscodeEntry(
|
||||
fun PasscodeView(password: MutableState<String>) {
|
||||
var showPasscode by rememberSaveable { mutableStateOf(false) }
|
||||
Text(
|
||||
if (password.value.isEmpty()) "" else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
|
||||
if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
|
||||
Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode },
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
@@ -47,7 +47,7 @@ fun PasscodeView(password: MutableState<String>) {
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) {
|
||||
val s = if (appPlatform.isAndroid) minOf(maxWidth, maxHeight) / 4 - 1.dp else minOf(minOf(maxWidth, maxHeight) / 4 - 1.dp, 100.dp)
|
||||
val s = minOf(maxWidth, maxHeight) / 4 - 1.dp
|
||||
Column(Modifier.width(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 1, 2, 3, password)
|
||||
Divider()
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
val maxWidth = with(LocalDensity.current) { screenWidth() * density }
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
-6
@@ -23,7 +23,6 @@ import chat.simplex.common.views.usersettings.IncognitoView
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
@Composable
|
||||
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
@@ -74,11 +73,6 @@ fun PasteToConnectLayout(
|
||||
title = generalGetString(MR.strings.invalid_connection_link),
|
||||
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
|
||||
)
|
||||
} catch (e: URISyntaxException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_connection_link),
|
||||
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -201,7 +201,6 @@ object AppearanceScope {
|
||||
val supportedLanguages = mapOf(
|
||||
"system" to generalGetString(MR.strings.language_system),
|
||||
"en" to "English",
|
||||
"bg" to "Български",
|
||||
"cs" to "Čeština",
|
||||
"de" to "Deutsch",
|
||||
"es" to "Español",
|
||||
@@ -214,7 +213,7 @@ object AppearanceScope {
|
||||
"ru" to "Русский",
|
||||
"zh-CN" to "简体中文"
|
||||
)
|
||||
val values by remember(ChatController.appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) }
|
||||
val values by remember { mutableStateOf(supportedLanguages.map { it.key to it.value }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
|
||||
values,
|
||||
@@ -228,9 +227,7 @@ object AppearanceScope {
|
||||
@Composable
|
||||
private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) {
|
||||
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
|
||||
val values by remember(ChatController.appPrefs.appLanguage.state.value) {
|
||||
mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third })
|
||||
}
|
||||
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.theme),
|
||||
values,
|
||||
|
||||
-18
@@ -66,24 +66,6 @@ fun PrivacySettingsView(
|
||||
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_chat_bubble),
|
||||
stringResource(MR.strings.privacy_show_last_messages),
|
||||
chatModel.controller.appPrefs.privacyShowChatPreviews,
|
||||
onChange = { showPreviews ->
|
||||
chatModel.showChatPreviews.value = showPreviews
|
||||
}
|
||||
)
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_edit_note),
|
||||
stringResource(MR.strings.privacy_message_draft),
|
||||
chatModel.controller.appPrefs.privacySaveLastDraft,
|
||||
onChange = { saveDraft ->
|
||||
if (!saveDraft) {
|
||||
chatModel.draft.value = null
|
||||
chatModel.draftChatId.value = null
|
||||
}
|
||||
})
|
||||
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">تم تغيير العنوان من أجلك</string>
|
||||
<string name="cant_delete_user_profile">لا يمكن حذف ملف تعريف المستخدم!</string>
|
||||
<string name="icon_descr_video_asked_to_receive">طلب لاستلام الفيديو</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string>
|
||||
<string name="callstatus_in_progress">مكالمتك تحت الإجراء</string>
|
||||
<string name="callstatus_ended">انتهت المكالمة</string>
|
||||
@@ -414,7 +414,7 @@
|
||||
<string name="settings_developer_tools">أدوات المطور</string>
|
||||
<string name="smp_server_test_delete_queue">حذف قائمة الانتظار</string>
|
||||
<string name="error_updating_user_privacy">خطأ في تحديث خصوصية المستخدم</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.</string>
|
||||
<string name="delete_profile">حذف ملف التعريف</string>
|
||||
<string name="smp_servers_delete_server">حذف الخادم</string>
|
||||
<string name="error_updating_link_for_group">خطأ في تحديث ارتباط المجموعة</string>
|
||||
@@ -498,7 +498,7 @@
|
||||
<string name="custom_time_unit_hours">ساعات</string>
|
||||
<string name="edit_history">السجل</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز الاستجابة السريعة في مكالمة الفيديو</b>، أو شارك الرابط.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز الاستجابة السريعة في مكالمة الفيديو</b>، أو شارك الرابط.</string>
|
||||
<string name="install_simplex_chat_for_terminal">ثبّت SimpleX Chat لطرفية</string>
|
||||
<string name="network_disable_socks_info">إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها.</string>
|
||||
<string name="hide_dev_options">إخفاء:</string>
|
||||
@@ -521,7 +521,7 @@
|
||||
<string name="info_menu">المعلومات</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">تحسن الخصوصية والأمان</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.</string>
|
||||
<string name="immune_to_spam_and_abuse">محصن ضد البريد العشوائي وسوء المعاملة</string>
|
||||
<string name="description_via_one_time_link_incognito">التخفي عبر رابط لمرة واحدة</string>
|
||||
<string name="icon_descr_image_snd_complete">أرسلت صورة</string>
|
||||
@@ -541,7 +541,7 @@
|
||||
<string name="onboarding_notifications_mode_service">فوري</string>
|
||||
<string name="host_verb">المضيف</string>
|
||||
<string name="hide_notification">إخفاء</string>
|
||||
<string name="turn_off_battery_optimization"><![CDATA[من أجل استخدامها، يرجى <b>تعطيل تحسين البطارية</b> لSimpleX في مربع الحوار التالي. وإلا، سيتم تعطيل الإخطارات.]]></string>
|
||||
<string name="turn_off_battery_optimization">من أجل استخدامها، يرجى <b>تعطيل تحسين البطارية</b> لSimpleX في مربع الحوار التالي. وإلا، سيتم تعطيل الإخطارات.</string>
|
||||
<string name="in_reply_to">ردًا على</string>
|
||||
<string name="icon_descr_instant_notifications">إشعارات فورية</string>
|
||||
<string name="enter_one_ICE_server_per_line">خوادم ICE (واحد لكل سطر)</string>
|
||||
@@ -716,7 +716,7 @@
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string>
|
||||
<string name="mark_code_verified">وضع علامة تَحقق منه</string>
|
||||
<string name="error_saving_user_password">خطأ في حفظ كلمة مرور المستخدم</string>
|
||||
<string name="many_people_asked_how_can_it_deliver"><![CDATA[سأل الكثير من الناس: <i>إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟</i>]]></string>
|
||||
<string name="many_people_asked_how_can_it_deliver">سأل الكثير من الناس: <i>إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟</i></string>
|
||||
<string name="error_saving_group_profile">خطأ في حفظ ملف تعريف المجموعة</string>
|
||||
<string name="notification_preview_mode_message">رسالة نصية</string>
|
||||
<string name="message_reactions">ردود فعل الرسائل</string>
|
||||
@@ -725,7 +725,7 @@
|
||||
<string name="message_delivery_error_title">خطأ في تسليم الرسالة</string>
|
||||
<string name="network_and_servers">الشبكة والخوادم</string>
|
||||
<string name="moderate_verb">إشراف</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b> ، ثم انقر فوق <b>اتصال</b> في التطبيق.]]></string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b> ، ثم انقر فوق <b>اتصال</b> في التطبيق.</string>
|
||||
<string name="share_text_moderated_at">تحت الإشراف في: %s</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">ردود الفعل الرسائل ممنوعة في هذه الدردشة.</string>
|
||||
<string name="moderated_item_description">مُشرف بواسطة %s</string>
|
||||
@@ -839,7 +839,7 @@
|
||||
<string name="call_connection_peer_to_peer">ند لند</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته</string>
|
||||
<string name="icon_descr_call_pending_sent">مكالمة في الانتظار</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل المرسلة باستخدام <b>تشفير ثنائي الطبقات من بين الطريفين</b>.]]></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل المرسلة باستخدام <b>تشفير ثنائي الطبقات من بين الطريفين</b>.</string>
|
||||
<string name="reset_color">إعادة تعيين الألوان</string>
|
||||
<string name="save_verb">حفظ</string>
|
||||
<string name="smp_servers_preset_address">عنوان الخادم المحدد مسبقًا</string>
|
||||
@@ -853,7 +853,7 @@
|
||||
<string name="onboarding_notifications_mode_title">الإشعارات خاصة</string>
|
||||
<string name="store_passphrase_securely_without_recover">يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها.</string>
|
||||
<string name="contact_developers">يرجى تحديث التطبيق والاتصال بالمطورين.</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">دليل المستخدم</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link">اقرأ المزيد في <font color=#0088ff>دليل المستخدم</font>.</string>
|
||||
<string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string>
|
||||
<string name="revoke_file__confirm">اسحب الوصول</string>
|
||||
<string name="save_archive">حفظ الأرشيف</string>
|
||||
@@ -878,7 +878,7 @@
|
||||
<string name="saved_ICE_servers_will_be_removed">ستتم إزالة خوادم WebRTC ICE المحفوظة.</string>
|
||||
<string name="prohibit_sending_files">منع إرسال الملفات والوسائط.</string>
|
||||
<string name="callstate_received_answer">استلمت إجابة…</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">مستودع GitHub</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link">اقرأ المزيد في <font color=#0088ff>مستودع GitHub</font>.</string>
|
||||
<string name="reject">رفض</string>
|
||||
<string name="relay_server_protects_ip">يحمي خادم الترحيل عنوان IP الخاص بك، ولكن يمكنه مراقبة مدة المكالمة.</string>
|
||||
<string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string>
|
||||
@@ -1090,7 +1090,7 @@
|
||||
<string name="stop_file__action">إيقاف الملف</string>
|
||||
<string name="stop_snd_file__title">التوقف عن استلام الملف؟</string>
|
||||
<string name="icon_descr_address">عنوان SimpleX</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported">اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.</string>
|
||||
<string name="share_with_contacts">مشاركة مع جهات الاتصال</string>
|
||||
<string name="shutdown_alert_question">إيقاف التشغيل؟</string>
|
||||
<string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string>
|
||||
@@ -1117,7 +1117,7 @@
|
||||
<string name="v4_4_french_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string>
|
||||
<string name="v4_6_audio_video_calls_descr">دعم البلوتوث وتحسينات أخرى.</string>
|
||||
<string name="v5_0_polish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[للحفاظ على خصوصيتك، بدلاً من دفع الإشعارات، يحتوي التطبيق على <b>خدمة SimpleX تعمل في الخلفية</b> – يستخدم نسبة قليلة من البطارية يوميًا.]]></string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">للحفاظ على خصوصيتك، بدلاً من دفع الإشعارات، يحتوي التطبيق على <b>خدمة SimpleX تعمل في الخلفية</b> – يستخدم نسبة قليلة من البطارية يوميًا.</string>
|
||||
<string name="tap_to_start_new_chat">انقر لبدء محادثة جديدة</string>
|
||||
<string name="to_share_with_your_contact">(للمشاركة مع جهة اتصالك)</string>
|
||||
<string name="to_connect_via_link_title">للتواصل عبر الرابط</string>
|
||||
@@ -1240,7 +1240,7 @@
|
||||
<string name="snd_group_event_user_left">غادرت</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[يمكنك التحكم من خلال الخادم (الخوادم) <b>لتلقي</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">يمكنك التحكم من خلال الخادم (الخوادم) <b>لتلقي</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string>
|
||||
<string name="snd_group_event_member_deleted">أُزيلت %1$s</string>
|
||||
<string name="update_database">تحديث</string>
|
||||
@@ -1293,7 +1293,7 @@
|
||||
<string name="unhide_profile">إلغاء إخفاء ملف تعريف</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال.
|
||||
\nيمكنك إلغاء هذا الاتصال وإزالة جهة الاتصال (والمحاولة لاحقًا باستخدام رابط جديد).</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[يمكنك أيضًا الاتصال بالضغط على الرابط. إذا تم فتحه في المتصفح، فانقر فوق الزر <b>فتح في تطبيق الجوال</b>.]]></string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">يمكنك أيضًا الاتصال بالضغط على الرابط. إذا تم فتحه في المتصفح، فانقر فوق الزر <b>فتح في تطبيق الجوال</b>.</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">استخدم للاتصالات الجديدة</string>
|
||||
<string name="smp_servers_use_server">استخدم الخادم</string>
|
||||
<string name="smp_servers_your_server_address">عنوان خادمك</string>
|
||||
@@ -1335,7 +1335,7 @@
|
||||
<string name="observer_cant_send_message_title">لا يمكنك إرسال رسائل!</string>
|
||||
<string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها.</string>
|
||||
<string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color=#0088ff>الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">يمكنك <font color=#0088ff>الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.</string>
|
||||
<string name="smp_servers_your_server">خادمك</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك.</string>
|
||||
<string name="icon_descr_video_off">الفيديو مقفل</string>
|
||||
|
||||
@@ -140,11 +140,11 @@
|
||||
<string name="turn_off_battery_optimization_button">Allow</string>
|
||||
<string name="turn_off_system_restriction_button">Open app settings</string>
|
||||
<string name="disable_notifications_button">Disable notifications</string>
|
||||
<string name="system_restricted_background_desc">SimpleX can\'t run in background. You will receive the notifications only when the app is running.</string>
|
||||
<string name="system_restricted_background_desc"><![CDATA[SimpleX can\'t run in background. You will receive the notifications only when the app is running.]]></string>
|
||||
<string name="system_restricted_background_warn"><![CDATA[To enable notifications, please choose <b>App battery usage</b> / <b>Unrestricted</b> in the app settings.]]></string>
|
||||
<string name="system_restricted_background_in_call_title">No background calls</string>
|
||||
<string name="system_restricted_background_in_call_desc">The app may be closed after 1 minute in background.</string>
|
||||
<string name="system_restricted_background_in_call_warn"><![CDATA[To make calls in background, please choose <b>App battery usage</b> / <b>Unrestricted</b> in the app settings.]]></string>
|
||||
<string name="system_restricted_background_in_call_desc"><![CDATA[The app may be closed after 1 minute in background.]]></string>
|
||||
<string name="system_restricted_background_in_call_warn"><![CDATA[To make calls in background, please choose <b>App battery usage</b> / <b>Unrestricted</b> in the app settings]]>.</string>
|
||||
<string name="enter_passphrase_notification_title">Passphrase is needed</string>
|
||||
<string name="enter_passphrase_notification_desc">To receive notifications, please, enter the database passphrase</string>
|
||||
<string name="database_initialization_error_title">Can\'t initialize the database</string>
|
||||
@@ -852,8 +852,6 @@
|
||||
<string name="protect_app_screen">Protect app screen</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
<string name="privacy_show_last_messages">Show last messages</string>
|
||||
<string name="privacy_message_draft">Message draft</string>
|
||||
<string name="full_backup">App data backup</string>
|
||||
<string name="enable_lock">Enable lock</string>
|
||||
<string name="lock_mode">Lock mode</string>
|
||||
@@ -1105,10 +1103,6 @@
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
|
||||
<string name="rcv_group_event_2_members_connected">%s and %s connected</string>
|
||||
<string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string>
|
||||
<string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
@@ -1562,4 +1556,4 @@
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<string name="accept_feature">Приеми</string>
|
||||
<string name="color_primary_variant">Допълнителен акцент</string>
|
||||
<string name="v4_2_group_links_desc">Админите могат да създадат линкове за присъединяване към групи.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Ако не можете да се срещнете лично, <b>покажете QR кода във видеоразговора</b> или споделете линка.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Ако не можете да се срещнете лично, <b>покажете QR кода във видеоразговора</b> или споделете линка.</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Отворете SimpleX Chat, за да приемете повикването</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Поддръжка на bluetooth и други подобрения.</string>
|
||||
<string name="relay_server_protects_ip">Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора.</string>
|
||||
@@ -87,9 +87,9 @@
|
||||
<string name="incognito_random_profile_from_contact_description">Произволен профил ще бъде изпратен до контакта, от който сте получили тази връзка</string>
|
||||
<string name="la_authenticate">Идентифицирай</string>
|
||||
<string name="turning_off_service_and_periodic">Оптимизацията на батерията е активна, изключват се фоновата услуга и периодичните заявки за нови съобщения. Можете да ги активирате отново през настройките.</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.]]></string>
|
||||
<string name="network_session_mode_user_description">Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.</string>
|
||||
<string name="icon_descr_audio_call">аудио разговор</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Най-добро за батерията</b>. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Най-добро за батерията</b>. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).</string>
|
||||
<string name="network_session_mode_entity_description">Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки контакт и член на група</b>.
|
||||
\n<b>Моля, обърнете внимание</b>: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.</string>
|
||||
<string name="icon_descr_asked_to_receive">Помолен да получи изображението</string>
|
||||
@@ -99,15 +99,15 @@
|
||||
<string name="both_you_and_your_contacts_can_delete">И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.</string>
|
||||
<string name="auth_unavailable">Идентификацията е недостъпна</string>
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">И вие, и вашият контакт можете да добавяте реакции към съобщението.</string>
|
||||
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Моля, обърнете внимание</b>: НЯМА да можете да възстановите или промените паролата, ако я загубите.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Използва повече батерия</b>! Услугата на заден план винаги работи – известията се показват веднага щом съобщенията са налични.]]></string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Моля, обърнете внимание</b>: НЯМА да можете да възстановите или промените паролата, ако я загубите.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Използва повече батерия</b>! Услугата на заден план винаги работи – известията се показват веднага щом съобщенията са налични.</string>
|
||||
<string name="integrity_msg_bad_hash">лош хеш на съобщението</string>
|
||||
<string name="alert_title_msg_bad_hash">Лош хеш на съобщението</string>
|
||||
<string name="no_call_on_lock_screen">Деактивиране</string>
|
||||
<string name="callstatus_calling">повикване…</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Ако не можете да се срещнете лично, можете <b>да сканирате QR код във видеоразговора</b> или вашият контакт може да сподели линк за покана.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Ако не можете да се срещнете лично, можете <b>да сканирате QR код във видеоразговора</b> или вашият контакт може да сподели линк за покана.</string>
|
||||
<string name="notifications_mode_service_desc">Услугата във фонов режим винаги работи – известията ще се показват веднага щом съобщенията са налични.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Може да бъде деактивирано през настройките</b> – известията ще продължат да се показват, докато приложението работи.]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Може да бъде деактивирано през настройките</b> – известията ще продължат да се показват, докато приложението работи.</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat разговори</string>
|
||||
<string name="notifications_mode_periodic">Започва периодично</string>
|
||||
<string name="database_initialization_error_title">Базата данни не може да се стартира</string>
|
||||
@@ -120,8 +120,8 @@
|
||||
<string name="back">Назад</string>
|
||||
<string name="cancel_verb">Отказ</string>
|
||||
<string name="icon_descr_cancel_live_message">Спри живото съобщение</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.]]></string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.</string>
|
||||
<string name="use_camera_button">Камера</string>
|
||||
<string name="if_you_cant_meet_in_person">Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</string>
|
||||
<string name="icon_descr_cancel_link_preview">спри визуализацията на линка</string>
|
||||
@@ -140,7 +140,7 @@
|
||||
<string name="callstatus_missed">пропуснато повикване</string>
|
||||
<string name="callstatus_rejected">отхвърлено повикване</string>
|
||||
<string name="callstate_starting">стартиране…</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Добър за батерията</b>. Фоновата услуга проверява съобщенията на всеки 10 минути. Може да пропуснете обаждания или спешни съобщения.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Добър за батерията</b>. Фоновата услуга проверява съобщенията на всеки 10 минути. Може да пропуснете обаждания или спешни съобщения.</string>
|
||||
<string name="callstate_connected">свързан</string>
|
||||
<string name="callstate_connecting">свързване…</string>
|
||||
<string name="encrypted_audio_call">e2e криптиран аудио разговор</string>
|
||||
@@ -441,7 +441,7 @@
|
||||
<string name="custom_time_unit_days">дни</string>
|
||||
<string name="choose_file_title">Избери файл</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Ако сте получили линк за покана за SimpleX Chat, можете да го отворите във вашия браузър:</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настолен компютър: сканирайте показания QR код от приложението чрез <b>Сканирай QR код</b>.]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 настолен компютър: сканирайте показания QR код от приложението чрез <b>Сканирай QR код</b>.</string>
|
||||
<string name="delete_pending_connection__question">Изтрий предстоящата връзка\?</string>
|
||||
<string name="icon_descr_email">Електронна поща</string>
|
||||
<string name="share_invitation_link">Сподели еднократен линк</string>
|
||||
@@ -452,7 +452,7 @@
|
||||
<string name="smp_servers_delete_server">Изтрий сървър</string>
|
||||
<string name="dont_create_address">Не създавай адрес</string>
|
||||
<string name="display_name__field">Показвано име:</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с <b>двуслойно криптиране от край до край</b>.]]></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с <b>двуслойно криптиране от край до край</b>.</string>
|
||||
<string name="receipts_contacts_title_disable">Деактивирай потвърждениeто\?</string>
|
||||
<string name="receipts_contacts_enable_keep_overrides">Активиране (запазване на промените)</string>
|
||||
<string name="receipts_contacts_title_enable">Активирай потвърждениeто\?</string>
|
||||
@@ -766,7 +766,7 @@
|
||||
<string name="invalid_message_format">невалиден формат на съобщението</string>
|
||||
<string name="invalid_connection_link">Невалиден линк за връзка</string>
|
||||
<string name="notification_display_mode_hidden_desc">Скриване на контакт и съобщение</string>
|
||||
<string name="turn_off_battery_optimization"><![CDATA[За да го използвате, моля, <b>деактивирайте оптимизирането на батерията</b> за SimpleX в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.]]></string>
|
||||
<string name="turn_off_battery_optimization">За да го използвате, моля, <b>деактивирайте оптимизирането на батерията</b> за SimpleX в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.</string>
|
||||
<string name="icon_descr_instant_notifications">Незабавни известия</string>
|
||||
<string name="service_notifications">Незабавни известия!</string>
|
||||
<string name="service_notifications_disabled">Незабавните известия са деактивирани!</string>
|
||||
@@ -791,14 +791,14 @@
|
||||
<string name="custom_time_unit_seconds">секунди</string>
|
||||
<string name="custom_time_unit_weeks">седмици</string>
|
||||
<string name="v5_1_japanese_portuguese_interface">Японски и португалски потребителски интерфейс</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобилно: докоснете <b>Отваряне в мобилно приложение</b>, след което докоснете <b>Свързване</b> в приложението.]]></string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 мобилно: докоснете <b>Отваряне в мобилно приложение</b>, след което докоснете <b>Свързване</b> в приложението.</string>
|
||||
<string name="reject_contact_button">Отхвърляне</string>
|
||||
<string name="mark_unread">Маркирай като непрочетено</string>
|
||||
<string name="mark_read">Маркирай като прочетено</string>
|
||||
<string name="mute_chat">Без звук</string>
|
||||
<string name="image_descr_qr_code">QR код</string>
|
||||
<string name="icon_descr_more_button">Повече</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color=#0088ff>Ръководство за потребителя</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link">Прочетете повече в <font color=#0088ff>Ръководство за потребителя</font>.</string>
|
||||
<string name="mark_code_verified">Маркирай като проверено</string>
|
||||
<string name="is_not_verified">%s не е потвърдено</string>
|
||||
<string name="is_verified">%s е потвърдено</string>
|
||||
@@ -818,7 +818,7 @@
|
||||
<string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string>
|
||||
<string name="password_to_show">Парола за показване</string>
|
||||
<string name="no_spaces">Без интервали!</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color=#0088ff>GitHub хранилище</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link">Прочетете повече в нашето <font color=#0088ff>GitHub хранилище</font>.</string>
|
||||
<string name="onboarding_notifications_mode_off">Когато приложението работи</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Периодично</string>
|
||||
<string name="paste_the_link_you_received">Постави получения линк</string>
|
||||
@@ -973,7 +973,7 @@
|
||||
<string name="simplex_service_notification_text">Получаване на съобщения…</string>
|
||||
<string name="notifications_mode_off">Работи, когато приложението е отворено</string>
|
||||
<string name="simplex_service_notification_title">Simplex Chat услуга</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[За да запази вашата поверителност, вместо да изпозлва push известия, приложението има <b> SimpleX фонова услуга </b> – използва няколко процента от батерията на ден.]]></string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">За да запази вашата поверителност, вместо да изпозлва push известия, приложението има <b> SimpleX фонова услуга </b> – използва няколко процента от батерията на ден.</string>
|
||||
<string name="enter_passphrase_notification_desc">За да получавате известия, моля, въведете паролата на базата данни</string>
|
||||
<string name="auth_log_in_using_credential">Влезте с вашите идентификационни данни</string>
|
||||
<string name="message_delivery_error_title">Грешка при доставката на съобщението</string>
|
||||
@@ -1001,7 +1001,7 @@
|
||||
<string name="privacy_redefined">Поверителността преосмислена</string>
|
||||
<string name="read_more_in_github">Прочетете повече в нашето хранилище в GitHub.</string>
|
||||
<string name="make_private_connection">Добави поверителна връзка</string>
|
||||
<string name="many_people_asked_how_can_it_deliver"><![CDATA[Много хора попитаха: <i>ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?</i>]]></string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Много хора попитаха: <i>ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?</i></string>
|
||||
<string name="open_verb">Отвори</string>
|
||||
<string name="relay_server_if_necessary">Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.</string>
|
||||
<string name="lock_after">Заключване след</string>
|
||||
@@ -1157,7 +1157,7 @@
|
||||
<string name="stop_file__action">Спри файл</string>
|
||||
<string name="icon_descr_settings">Настройки</string>
|
||||
<string name="star_on_github">Звезда в GitHub</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Задайте <i>Използване на .onion хостове</i> на Не, ако SOCKS проксито не ги поддържа.]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Задайте <i>Използване на .onion хостове</i> на Не, ако SOCKS проксито не ги поддържа.</string>
|
||||
<string name="show_dev_options">Покажи:</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="save_and_notify_contact">Запази и уведоми контакта</string>
|
||||
@@ -1257,7 +1257,7 @@
|
||||
\nМожете да го промените в Настройки.</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Можете да използвате markdown за форматиране на съобщенията:</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Вие контролирате през кой сървър(и) <b>да получавате</b> съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.]]></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Вие контролирате през кой сървър(и) <b>да получавате</b> съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.</string>
|
||||
<string name="use_chat">Използвай чата</string>
|
||||
<string name="update_database">Актуализация</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството.</string>
|
||||
@@ -1351,9 +1351,9 @@
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</string>
|
||||
<string name="you_are_observer">вие сте наблюдател</string>
|
||||
<string name="gallery_video_button">Видео</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color=#0088ff>се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Можете да <font color=#0088ff>се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>.</string>
|
||||
<string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.]]></string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.</string>
|
||||
<string name="xftp_servers">XFTP сървъри</string>
|
||||
<string name="update_network_session_mode_question">Актуализиране на режима на изолация на транспорта\?</string>
|
||||
<string name="your_current_profile">Вашият текущ профил</string>
|
||||
|
||||
@@ -762,7 +762,7 @@
|
||||
<string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string>
|
||||
<string name="messages_section_description">Esta configuración se aplica a los mensajes del perfil actual</string>
|
||||
<string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.]]></string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.</string>
|
||||
<string name="icon_descr_settings">Configuración</string>
|
||||
<string name="icon_descr_speaker_off">Altavoz desactivado</string>
|
||||
<string name="add_contact_or_create_group">Inciar chat nuevo</string>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M241.776-244.5 134-136.5q-13.5 13.5-31.25 6.359Q85-137.281 85-156.5V-818q0-22.969 17.266-40.234Q119.531-875.5 142.5-875.5h675q22.969 0 40.234 17.266Q875-840.969 875-818v516q0 22.969-17.266 40.234Q840.469-244.5 817.5-244.5H241.776ZM142.5-302h675v-516h-675v516Zm0 0v-516 516Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 379 B |
@@ -763,7 +763,7 @@
|
||||
<string name="wrong_passphrase">Password del database sbagliata</string>
|
||||
<string name="wrong_passphrase_title">Password sbagliata!</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sei stato/a invitato/a al gruppo. Entra per connetterti con i suoi membri.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni / Database o riavviando l\'app.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni -> Database o riavviando l\'app.</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante.</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata.</string>
|
||||
<string name="group_member_status_invited">ha invitato</string>
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
<string name="v5_0_large_files_support">Vaizdo įrašai ir failai iki 1GB</string>
|
||||
<string name="search_verb">Ieškoti</string>
|
||||
<string name="la_mode_off">Išjungta</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam pokalbių profiliui, kurį turite programoje</b>.]]></string>
|
||||
<string name="network_session_mode_user_description">Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam pokalbių profiliui, kurį turite programoje</b>.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nepavyko iššifruoti %1$d pranešimų.</string>
|
||||
<string name="button_add_welcome_message">Pridėti sveikinimo pranešimą</string>
|
||||
<string name="v5_2_more_things">Dar keletas dalykų</string>
|
||||
@@ -539,7 +539,7 @@
|
||||
<string name="always_use_relay">Visada naudoti relę</string>
|
||||
<string name="icon_descr_asked_to_receive">Paprašė leidimo gauti nuotrauką</string>
|
||||
<string name="send_disappearing_message_5_minutes">5 minučių</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Mažiausiai bateriją eikvojantis variantas</b>. Jūs gausite sistemos pranešimus tik kai bus atidaryta programėlė (NEBUS fone veikiančios paslaugos).]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Mažiausiai bateriją eikvojantis variantas</b>. Jūs gausite sistemos pranešimus tik kai bus atidaryta programėlė (NEBUS fone veikiančios paslaugos).</string>
|
||||
<string name="abort_switch_receiving_address">Atšaukti adreso keitimą</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Automatiškai priimti susisiekimo užklausas</string>
|
||||
<string name="v5_1_self_destruct_passcode_descr">Kai jį suvedate visi duomenys yra pašalinami.</string>
|
||||
@@ -603,19 +603,19 @@
|
||||
<string name="one_time_link_short">vienkartinė nuoroda</string>
|
||||
<string name="accept_call_on_lock_screen">Priimti</string>
|
||||
<string name="auto_accept_contact">Automatiškai priimti</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Geriau baterijai</b>. Fono paslauga tikrina pranešimus kas 10 minučių. Galite praleisti skambučius arba skubius pranešimus.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Geriau baterijai</b>. Fono paslauga tikrina pranešimus kas 10 minučių. Galite praleisti skambučius arba skubius pranešimus.</string>
|
||||
<string name="callstatus_in_progress">vyksta skambutis</string>
|
||||
<string name="icon_descr_cancel_live_message">Atšaukti tiesioginę žinutę</string>
|
||||
<string name="alert_title_cant_invite_contacts">Nepavyko pakviesti kontaktų!</string>
|
||||
<string name="icon_descr_call_progress">Vyksta skambutis</string>
|
||||
<string name="feature_cancelled_item">atšaukta %s</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Galima išjungti nustatymuose</b> - pranešimai vis tiek bus rodomi kol programėlė veikia.]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Galima išjungti nustatymuose</b> - pranešimai vis tiek bus rodomi kol programėlė veikia.</string>
|
||||
<string name="icon_descr_cancel_link_preview">atšaukti nuorodos peržiūrą</string>
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">Jūs ir jūsų kontaktas galite pridėti reakcijas į žinutę.</string>
|
||||
<string name="call_on_lock_screen">Skambučiai užrakinimo ekrane:</string>
|
||||
<string name="invite_prohibited">Nepavyko pakviesti kontakto!</string>
|
||||
<string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.</string>
|
||||
<string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string>
|
||||
<string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string>
|
||||
<string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<string name="incognito_random_profile_from_contact_description">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อที่คุณได้รับลิงก์นี้จาก</string>
|
||||
<string name="incognito_random_profile_description">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อของคุณ</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับแต่ละโปรไฟล์แชทที่คุณมีในแอป </b>]]></string>
|
||||
<string name="network_session_mode_entity_description"><![CDATA[การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่มแต่ละคน </b>\n<b>โปรดทราบ </b>: หากคุณมีการเชื่อมต่อจํานวนมากแบตเตอรี่และปริมาณการใช้การจราจรของคุณอาจสูงขึ้นอย่างมากและการเชื่อมต่อบางอย่างอาจล้มเหลว]]></string>
|
||||
<string name="network_session_mode_entity_description">การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่มแต่ละคน </b>\n<b>โปรดทราบ </b>: หากคุณมีการเชื่อมต่อจํานวนมากแบตเตอรี่และปริมาณการใช้การจราจรของคุณอาจสูงขึ้นอย่างมากและการเชื่อมต่อบางอย่างอาจล้มเหลว</string>
|
||||
<string name="attach">แนบ</string>
|
||||
<string name="v4_6_audio_video_calls">การโทรด้วยเสียงและวิดีโอ</string>
|
||||
<string name="auto_accept_contact">ยอมรับอัตโนมัติ</string>
|
||||
@@ -530,7 +530,7 @@
|
||||
<string name="error_receiving_file">เกิดข้อผิดพลาดในการรับไฟล์</string>
|
||||
<string name="if_you_enter_passcode_data_removed">หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">หากคุณเลือกที่จะปฏิเสธ ผู้ส่งจะไม่ได้รับแจ้ง</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">หากคุณไม่สามารถพบกันในชีวิตจริงได้ คุณสามารถสแกนคิวอาร์โค้ดในวิดีโอคอล หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[หากคุณไม่สามารถพบกันในชีวิตจริงได้ คุณสามารถสแกนคิวอาร์โค้ดในวิดีโอคอล </b> หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[หากคุณไม่สามารถพบกันในชีวิตจริงได้ <b>ให้แสดงคิวอาร์โค้ดในวิดีโอคอล</b> หรือแชร์ลิงก์]]></string>
|
||||
<string name="if_you_cant_meet_in_person">หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</string>
|
||||
<string name="network_disable_socks_info">หากคุณยืนยัน เซิร์ฟเวอร์การส่งข้อความจะสามารถเห็นที่อยู่ IP ของคุณและผู้ให้บริการของคุณ - ซึ่งคือเซิร์ฟเวอร์ใดที่คุณกำลังเชื่อมต่ออยู่</string>
|
||||
@@ -927,7 +927,7 @@
|
||||
<string name="smp_servers_scan_qr">สแกนคิวอาร์โค้ดของเซิร์ฟเวอร์</string>
|
||||
<string name="smp_save_servers_question">บันทึกเซิร์ฟเวอร์\?</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[ตั้ง <i>ใช้โฮสต์ .onion</i> เป็น ไม่ หากพร็อกซี SOCKS ไม่รองรับ]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported">ตั้ง <i>ใช้โฮสต์ .onion</i> เป็น ไม่ หากพร็อกซี SOCKS ไม่รองรับ</string>
|
||||
<string name="show_dev_options">แสดง:</string>
|
||||
<string name="show_developer_options">แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์</string>
|
||||
<string name="share_link">แชร์ลิงก์</string>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<string name="all_group_members_will_remain_connected">Konuşma üyelerinin tümü bağlı kalacaktır.</string>
|
||||
<string name="allow_verb">İzin ver</string>
|
||||
<string name="v5_0_app_passcode">Uygulama erişim kodu</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[<b>Uygulamadaki her konuşma profliniz için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.]]></string>
|
||||
<string name="network_session_mode_user_description"><b>Uygulamadaki her konuşma profliniz için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.</string>
|
||||
<string name="network_session_mode_entity_description"><b>Konuştuğun kişilerin ve grup üyelerinin tamamı için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.
|
||||
\n<b>Bilgin olsun</b>: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir.</string>
|
||||
<string name="save_and_notify_group_members">Kaydet ve grup üyelerini bilgilendir</string>
|
||||
@@ -391,7 +391,7 @@
|
||||
<string name="send_disappearing_message_custom_time">Kişiselleştirilmiş süre</string>
|
||||
<string name="copied">Panoya kopyalandı</string>
|
||||
<string name="share_one_time_link">Tek seferlik davet bağlantısı oluştur</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 masaüstü: uygulamadaki <b>Karekodu okut</b> ile karekodu okut]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 masaüstü: uygulamadaki <b>Karekodu okut</b> ile karekodu okut</string>
|
||||
<string name="delete_pending_connection__question">Bekleyen bağlantıları sil\?</string>
|
||||
<string name="delete_contact_menu_action">Sil</string>
|
||||
<string name="delete_group_menu_action">Sil</string>
|
||||
@@ -625,8 +625,8 @@
|
||||
<string name="notification_display_mode_hidden_desc">Konuşulan kişileri ve mesajları gizle</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Uygulamayı, son kullanılanlar kısmından gizle.</string>
|
||||
<string name="how_simplex_works">SimpleX nasıl çalışıyor</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakine karekodunu gösterebilir</b> ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakinin karekodunu okutabilirsin</b> ya da konuştuğun kişi seninle bir katılım bağlantısı paylaşabilir.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakine karekodunu gösterebilir</b> ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakinin karekodunu okutabilirsin</b> ya da konuştuğun kişi seninle bir katılım bağlantısı paylaşabilir.</string>
|
||||
<string name="if_you_cant_meet_in_person">Eğer yüz yüze görüşemiyorsanız bir görüntülü aramada karşıdakine karekodunu gösterebilir ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Eğer geri çevirmeyi seçersen göndericiye bildirilmeyecek.</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Eğer SimplexX Chat katılım bağlantısı alırsan bu bağlantıyı tarayıcında açabilirsin:</string>
|
||||
@@ -717,7 +717,7 @@
|
||||
<string name="icon_descr_instant_notifications">Anlık bildirimler</string>
|
||||
<string name="service_notifications">Anlık bildirimler</string>
|
||||
<string name="service_notifications_disabled">Anlık bildirimler devre dışı!</string>
|
||||
<string name="turn_off_battery_optimization"><![CDATA[Bunu kullanmak için lütfen bir sonraki iletişim kutusunda SimpleX için <b>pil optimizasyonunu devre dışı bırakın</b>. Aksi takdirde, bildirimler devre dışı bırakılacaktır.]]></string>
|
||||
<string name="turn_off_battery_optimization">Bunu kullanmak için lütfen bir sonraki iletişim kutusunda SimpleX için <b>pil optimizasyonunu devre dışı bırakın</b>. Aksi takdirde, bildirimler devre dışı bırakılacaktır.</string>
|
||||
<string name="notification_preview_mode_contact">Kişi ismi</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Cihaz doğrulaması devre dışı. SimpleX Kilidi Kapatılıyor.</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Cihaz doğrulaması etkin değil. Cihaz doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</string>
|
||||
@@ -740,7 +740,7 @@
|
||||
<string name="v4_3_irreversible_message_deletion">Geri alınamaz mesaj silme</string>
|
||||
<string name="v4_5_italian_interface">İtalyanca arayüz</string>
|
||||
<string name="choose_file_title">Dosya seç</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR kodunu tara</b>: size QR kodunu gösteren kişiyle bağlantı kurmak için.]]></string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR kodunu tara</b>: size QR kodunu gösteren kişiyle bağlantı kurmak için.</string>
|
||||
<string name="invite_friends">Arkadaşlarınızı davet edin</string>
|
||||
<string name="bold_text">kalın</string>
|
||||
<string name="italic_text">İtalik</string>
|
||||
@@ -748,7 +748,7 @@
|
||||
<string name="status_contact_has_e2e_encryption">Kişi uçtan uca şifrelemeye sahiptir</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">Kişi uçtan uca şifrelemeye sahip değildir</string>
|
||||
<string name="chat_is_stopped">Sohbet durduruldu</string>
|
||||
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]></string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.</string>
|
||||
<string name="chat_archive_header">Sohbet arşivi</string>
|
||||
<string name="chat_archive_section">SOHBET ARŞİVİ</string>
|
||||
<string name="group_invitation_item_description">1$s grubuna davet</string>
|
||||
@@ -767,7 +767,7 @@
|
||||
<string name="callstatus_connecting">Aramaya bağlanılıyor…</string>
|
||||
<string name="delivery_receipts_are_disabled">Gönderildi bilgisi kapalı!</string>
|
||||
<string name="v5_2_more_things">Birkaç şey daha</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Daha fazla pil kullanır</b>! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.</string>
|
||||
<string name="in_developing_title">Çok yakında!</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kişi ve tüm mesajlar silinecektir - bu geri alınamaz!</string>
|
||||
<string name="alert_title_contact_connection_pending">Kişi henüz bağlanmadı!</string>
|
||||
@@ -783,7 +783,7 @@
|
||||
<string name="description_via_contact_address_link_incognito">Bağlantı linki ile gizli</string>
|
||||
<string name="description_via_one_time_link_incognito">Tek seferlik bağlantı ile gizli</string>
|
||||
<string name="smp_server_test_compare_file">Dosyaları karşılaştır</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>ayarlardan devre dışı bırakılabilir</b> - uygulama çalışıyorken bildirimler gösterilmeye devam edilecektir.]]></string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>ayarlardan devre dışı bırakılabilir</b> - uygulama çalışıyorken bildirimler gösterilmeye devam edilecektir.</string>
|
||||
<string name="turning_off_service_and_periodic">Pil optimizasyonu etkin, arka plan hizmeti kapatılacak ve düzenli olarak yeni mesajlar kontrol edilmeyecek . Bunları ayarlardan yeniden etkinleştirebilirsiniz.</string>
|
||||
<string name="enter_passphrase_notification_title">Parola gerekli</string>
|
||||
<string name="notification_preview_mode_message">Mesaj metni</string>
|
||||
@@ -794,7 +794,7 @@
|
||||
<string name="delete_message_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string>
|
||||
<string name="switch_receiving_address_question">Alıcı adresini değiştir\?</string>
|
||||
<string name="back">Geri</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Yeni kişi ekle</b>: Kişiniz için tek seferlik QR Kodunuzu oluşturmak için.]]></string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Yeni kişi ekle</b>: Kişiniz için tek seferlik QR Kodunuzu oluşturmak için.</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string>
|
||||
<string name="learn_more">Daha fazla bilgi edinin</string>
|
||||
@@ -836,10 +836,10 @@
|
||||
<string name="mark_read">Okundu olarak işaretle</string>
|
||||
<string name="mark_unread">Okunmadı olarak işaretle</string>
|
||||
<string name="chat_console">Sohbet konsolu</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil için iyi</b>. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Pil için iyi</b>. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Güvenlik kodlarını kişilerinizle karşılaştırın.</string>
|
||||
<string name="notifications_mode_service_desc">Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Pil için en iyisi</b>. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Pil için en iyisi</b>. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s için adres değiştiriliyor.</string>
|
||||
<string name="large_file">Büyük dosya!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string>
|
||||
@@ -869,7 +869,7 @@
|
||||
<string name="your_ICE_servers">ICE sunucularınız</string>
|
||||
<string name="how_to">Nasıl</string>
|
||||
<string name="your_current_profile">Mevcut profiliniz</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Mesajların hangi sunucu(lar)dan <b>alınacağını</b> siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.]]></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Mesajların hangi sunucu(lar)dan <b>alınacağını</b> siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.</string>
|
||||
<string name="video_call_no_encryption">video arama (uçtan uca şifreli değil)</string>
|
||||
<string name="your_ice_servers">ICE sunucularınız</string>
|
||||
<string name="icon_descr_video_off">Video kapalı</string>
|
||||
@@ -908,8 +908,8 @@
|
||||
\nBu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz).</string>
|
||||
<string name="contact_sent_large_file">Kişiniz desteklenen maksimum boyuttan (%1$s) daha büyük bir dosya gönderdi.</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Kişiniz yüklemeyi tamamladığında video alınacaktır.</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ayrıca bağlantıya tıklayarak da bağlanabilirsiniz. Eğer bağlantı tarayıcda açılırsa, <b>mobil uygulamada aç</b> seçeneğine tıklayın.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color=#0088ff>SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Ayrıca bağlantıya tıklayarak da bağlanabilirsiniz. Eğer bağlantı tarayıcda açılırsa, <b>mobil uygulamada aç</b> seçeneğine tıklayın.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Soru sormak ve güncellemeleri almak için <font color=#0088ff>SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Bir kullanıcının profilini gizleyebilir veya sessize alabilirsiniz - menü için basılı tutun.</string>
|
||||
<string name="you_invited_your_contact">Kişinizi davet ettiniz</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz.</string>
|
||||
@@ -957,7 +957,7 @@
|
||||
\n- ve daha fazlası!</string>
|
||||
<string name="item_status_rcv_new_desc">Bu göndericiden yeni bir mesaj var.</string>
|
||||
<string name="item_status_snd_error_auth_desc">Mesaj teslim hatası. Büyük olasılıkla bu alıcı sizinle olan bağlantısını silmiştir.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Sadece istemci cihazlar <b>2 katmanlı uçtan uca şifreleme</b> ile kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.]]></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Sadece istemci cihazlar <b>2 katmanlı uçtan uca şifreleme</b> ile kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Grup tercihlerini sadece grup sahipleri değiştirebilir.</string>
|
||||
<string name="item_info_no_text">metin yok</string>
|
||||
<string name="network_status">Ağ durumu</string>
|
||||
@@ -966,7 +966,7 @@
|
||||
<string name="change_lock_mode">Kilit modunu değiştir</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor.</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya irtibat kişinizden size başka bir bağlantı göndermesini isteyin.</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Gizliliğinizi korumak için, anlık bildirimler yerine <b>SimpleX arka plan hizmeti</b> kullanılır - günde pilin yüzde birkaçını kullanır.]]></string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Gizliliğinizi korumak için, anlık bildirimler yerine <b>SimpleX arka plan hizmeti</b> kullanılır - günde pilin yüzde birkaçını kullanır.</string>
|
||||
<string name="periodic_notifications">Periyodik bildirimler</string>
|
||||
<string name="periodic_notifications_disabled">Periyodik bildirimler devre dışı</string>
|
||||
<string name="enter_passphrase_notification_desc">Bildirimleri almak için lütfen veri tabanı parolasını girin</string>
|
||||
@@ -988,7 +988,7 @@
|
||||
<string name="this_link_is_not_a_valid_connection_link">Bu geçerli bir bağlantı linki değil</string>
|
||||
<string name="this_QR_code_is_not_a_link">Bu QR kodu bir bağlantı değil!</string>
|
||||
<string name="paste_connection_link_below_to_connect">Kişinizle bağlantı kurmak için aldığınız bağlantıyı aşağıdaki kutuya yapıştırın.</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color=#0088ff>Kullanıcı Kılavuzu</font>.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link">Daha fazla bilgi için <font color=#0088ff>Kullanıcı Kılavuzu</font>.</string>
|
||||
<string name="paste_button">Yapıştır</string>
|
||||
<string name="this_string_is_not_a_connection_link">Bu dize bir bağlantı linki değil!</string>
|
||||
<string name="rate_the_app">Uygulamaya puan verin</string>
|
||||
@@ -1033,7 +1033,7 @@
|
||||
<string name="unfavorite_chat">Favorilerden çıkar</string>
|
||||
<string name="make_profile_private">Sohbeti gizli yap!</string>
|
||||
<string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi kişilerinize gönderilecektir.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[<font color=#0088ff>GitHub repomuzda</font> daha fazlasını okuyun.]]></string>
|
||||
<string name="read_more_in_github_with_link"><font color=#0088ff>GitHub repomuzda</font> daha fazlasını okuyun.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Lütfen geliştiricilere bildirin.</string>
|
||||
<string name="users_delete_with_connections">Profil ve sunucu bağlantıları</string>
|
||||
<string name="user_unhide">gizlemeyi kaldır</string>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<string name="notifications_mode_off_desc">Додаток може отримувати сповіщення лише під час роботи, жодні фонові служби не запускаються</string>
|
||||
<string name="chat_preferences_always">завжди</string>
|
||||
<string name="allow_your_contacts_to_call">Дозвольте вашим контактам телефонувати вам.</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[<b>Для кожного профілю чату, який ви маєте в додатку</b>, буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис).]]></string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Для кожного профілю чату, який ви маєте в додатку</b>, буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис) <b>.]]></string>
|
||||
<string name="appearance_settings">Зовнішній вигляд</string>
|
||||
<string name="app_version_name">Версія програми: v%s</string>
|
||||
<string name="network_session_mode_entity_description"><b>Для кожного контакту і члена групи</b> буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис).
|
||||
|
||||
+68
-76
@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.defaultLocale
|
||||
import chat.simplex.common.platform.desktopPlatform
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.views.helpers.FileDialogChooser
|
||||
@@ -26,96 +25,89 @@ import java.io.File
|
||||
val simplexWindowState = SimplexWindowState()
|
||||
|
||||
fun showApp() = application {
|
||||
// TODO: remove after update to compose 1.5.0+
|
||||
// See: https://github.com/JetBrains/compose-multiplatform/issues/3366#issuecomment-1643799976
|
||||
System.setProperty("compose.scrolling.smooth.enabled", "false")
|
||||
|
||||
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
||||
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
||||
val width = if (desktopPlatform.isLinux()) 1376.dp else 1366.dp
|
||||
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = width, height = 768.dp)
|
||||
simplexWindowState.windowState = windowState
|
||||
// Reload all strings in all @Composable's after language change at runtime
|
||||
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
||||
Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = {
|
||||
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
|
||||
simplexWindowState.backstack.lastOrNull()?.invoke() != null
|
||||
} else {
|
||||
false
|
||||
Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = {
|
||||
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
|
||||
simplexWindowState.backstack.lastOrNull()?.invoke() != null
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}, title = "SimpleX") {
|
||||
SimpleXTheme {
|
||||
AppScreen()
|
||||
if (simplexWindowState.openDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openDialog.onResult(it.firstOrNull())
|
||||
}
|
||||
)
|
||||
}
|
||||
}, title = "SimpleX") {
|
||||
SimpleXTheme {
|
||||
AppScreen()
|
||||
if (simplexWindowState.openDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openDialog.onResult(it.firstOrNull())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (simplexWindowState.openMultipleDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openMultipleDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openMultipleDialog.onResult(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (simplexWindowState.openMultipleDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = true,
|
||||
params = simplexWindowState.openMultipleDialog.params,
|
||||
onResult = {
|
||||
simplexWindowState.openMultipleDialog.onResult(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (simplexWindowState.saveDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = false,
|
||||
params = simplexWindowState.saveDialog.params,
|
||||
onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) }
|
||||
if (simplexWindowState.saveDialog.isAwaiting) {
|
||||
FileDialogChooser(
|
||||
title = "SimpleX",
|
||||
isLoad = false,
|
||||
params = simplexWindowState.saveDialog.params,
|
||||
onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) }
|
||||
)
|
||||
}
|
||||
val toasts = remember { simplexWindowState.toasts }
|
||||
val toast = toasts.firstOrNull()
|
||||
if (toast != null) {
|
||||
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
|
||||
Text(
|
||||
toast.first,
|
||||
Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
val toasts = remember { simplexWindowState.toasts }
|
||||
val toast = toasts.firstOrNull()
|
||||
if (toast != null) {
|
||||
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
|
||||
Text(
|
||||
toast.first,
|
||||
Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
// Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires
|
||||
LaunchedEffect(toast, toasts.size) {
|
||||
delay(toast.second)
|
||||
simplexWindowState.toasts.removeFirst()
|
||||
}
|
||||
// Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires
|
||||
LaunchedEffect(toast, toasts.size) {
|
||||
delay(toast.second)
|
||||
simplexWindowState.toasts.removeFirst()
|
||||
}
|
||||
}
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
LaunchedEffect(windowFocused) {
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
|
||||
delay(delay * 1000L)
|
||||
// Trigger auth state check when delay ends (and if it ends)
|
||||
}
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
LaunchedEffect(windowFocused) {
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
|
||||
delay(delay * 1000L)
|
||||
// Trigger auth state check when delay ends (and if it ends)
|
||||
AppLock.recheckAuthState()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
window.addWindowFocusListener(object : WindowFocusListener {
|
||||
override fun windowGainedFocus(p0: WindowEvent?) {
|
||||
windowFocused = true
|
||||
AppLock.recheckAuthState()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
window.addWindowFocusListener(object: WindowFocusListener {
|
||||
override fun windowGainedFocus(p0: WindowEvent?) {
|
||||
windowFocused = true
|
||||
AppLock.recheckAuthState()
|
||||
}
|
||||
|
||||
override fun windowLostFocus(p0: WindowEvent?) {
|
||||
windowFocused = false
|
||||
AppLock.appWasHidden()
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun windowLostFocus(p0: WindowEvent?) {
|
||||
windowFocused = false
|
||||
AppLock.appWasHidden()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -36,15 +36,18 @@ private val settingsThemesProps =
|
||||
actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") }
|
||||
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") }
|
||||
|
||||
actual fun windowOrientation(): WindowOrientation =
|
||||
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
|
||||
WindowOrientation.LANDSCAPE
|
||||
} else {
|
||||
WindowOrientation.PORTRAIT
|
||||
}
|
||||
actual fun screenOrientation(): ScreenOrientation = ScreenOrientation.UNDEFINED
|
||||
|
||||
@Composable
|
||||
actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width
|
||||
@Composable // LALAL
|
||||
actual fun screenWidth(): Dp {
|
||||
return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp.also { println("LALAL $it") }
|
||||
/*var width by remember { mutableStateOf(java.awt.Toolkit.getDefaultToolkit().screenSize.width.also { println("LALAL $it") }) }
|
||||
SideEffect {
|
||||
if (width != java.awt.Toolkit.getDefaultToolkit().screenSize.width)
|
||||
width = java.awt.Toolkit.getDefaultToolkit().screenSize.width
|
||||
}
|
||||
return width*/
|
||||
}// LALAL java.awt.Desktop.getDesktop()
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {
|
||||
if (simplexWindowState.windowState.size.width >= width) return
|
||||
|
||||
@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.version_name=5.3-beta.5
|
||||
android.version_code=147
|
||||
android.version_name=5.3-beta.3
|
||||
android.version_code=141
|
||||
|
||||
desktop.version_name=1.3.0
|
||||
desktop.version_code=5
|
||||
desktop.version_name=1.1.0
|
||||
desktop.version_code=3
|
||||
|
||||
kotlin.version=1.8.20
|
||||
gradle.plugin.version=7.4.2
|
||||
|
||||
@@ -19,7 +19,7 @@ import Control.Monad.Reader
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.LocalTime (getCurrentTimeZone)
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
import Data.Maybe (fromMaybe, isJust, maybeToList)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -134,7 +134,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {
|
||||
_ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
|
||||
|
||||
deContactConnected :: Contact -> IO ()
|
||||
deContactConnected ct = when (contactDirect ct) $ do
|
||||
deContactConnected ct = unless (isJust $ viaGroup ct) $ do
|
||||
unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected"
|
||||
sendMessage cc ct $
|
||||
"Welcome to " <> serviceName <> " service!\n\
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: cf2a17b80ce5736a8b3b02016e3f466f781f259d
|
||||
tag: 82aec2cd8f7b4033dbf08d5de33ced216f574bbb
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
+12
-102
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Hosting your own SMP Server
|
||||
revision: 31.07.2023
|
||||
revision: 05.06.2023
|
||||
---
|
||||
|
||||
| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
|
||||
@@ -19,27 +19,17 @@ _Please note_: when you change the servers in the app configuration, it only aff
|
||||
|
||||
0. First, install `smp-server`:
|
||||
|
||||
- Manual deployment (see below)
|
||||
- Manual deployment:
|
||||
|
||||
- Semi-automatic deployment:
|
||||
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
|
||||
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
||||
- [Compiling from source](https://github.com/simplex-chat/simplexmq#using-your-distribution)
|
||||
- [Using pre-compiled binaries](https://github.com/simplex-chat/simplexmq#install-binaries)
|
||||
|
||||
- Alternatively, you can deploy `smp-server` using:
|
||||
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker-1)
|
||||
- [Linode StackScript](https://github.com/simplex-chat/simplexmq#deploy-smp-server-on-linode)
|
||||
|
||||
Manual installation requires some preliminary actions:
|
||||
|
||||
0. Install binary:
|
||||
|
||||
- Using offical binaries:
|
||||
|
||||
```sh
|
||||
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server
|
||||
```
|
||||
|
||||
- Compiling from source:
|
||||
|
||||
Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution)
|
||||
|
||||
1. Create user and group for `smp-server`:
|
||||
|
||||
```sh
|
||||
@@ -67,104 +57,24 @@ Manual installation requires some preliminary actions:
|
||||
|
||||
```sh
|
||||
[Unit]
|
||||
Description=SMP server systemd service
|
||||
|
||||
Description=SMP server
|
||||
[Service]
|
||||
User=smp
|
||||
Group=smp
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS
|
||||
ExecStart=smp-server start
|
||||
ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex/smp-server-store.log" ] && cp "/var/opt/simplex/smp-server-store.log" "/var/opt/simplex/smp-server-store.log.bak"'
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=65535
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
And execute `sudo systemctl daemon-reload`.
|
||||
|
||||
## Tor installation
|
||||
|
||||
smp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user.
|
||||
|
||||
1. Install tor:
|
||||
|
||||
We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide.
|
||||
|
||||
- Configure offical Tor PPA repository:
|
||||
|
||||
```sh
|
||||
CODENAME="$(lsb_release -c | awk '{print $2}')"
|
||||
echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main
|
||||
deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list
|
||||
```
|
||||
|
||||
- Import repository key:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null
|
||||
```
|
||||
|
||||
- Update repository index:
|
||||
|
||||
```sh
|
||||
apt update
|
||||
```
|
||||
|
||||
- Install `tor` package:
|
||||
|
||||
```sh
|
||||
apt install -y tor deb.torproject.org-keyring
|
||||
```
|
||||
|
||||
2. Configure tor:
|
||||
|
||||
- File configuration:
|
||||
|
||||
Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.):
|
||||
|
||||
```sh
|
||||
vim /etc/tor/torrc
|
||||
```
|
||||
|
||||
And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options.
|
||||
|
||||
```sh
|
||||
# Enable log (otherwise, tor doesn't seemd to deploy onion address)
|
||||
Log notice file /var/log/tor/notices.log
|
||||
# Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside smp-server and onion address will be displayed in clients, this is totally fine)
|
||||
SOCKSPort 0
|
||||
HiddenServiceNonAnonymousMode 1
|
||||
HiddenServiceSingleHopMode 1
|
||||
# smp-server hidden service host directory and port mappings
|
||||
HiddenServiceDir /var/lib/tor/simplex-smp/
|
||||
HiddenServicePort 5223 localhost:5223
|
||||
```
|
||||
|
||||
- Create directories:
|
||||
|
||||
```sh
|
||||
mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/
|
||||
```
|
||||
|
||||
3. Start tor:
|
||||
|
||||
Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case.
|
||||
|
||||
```sh
|
||||
systemctl enable tor && systemctl start tor && systemctl restart tor
|
||||
```
|
||||
|
||||
4. Display onion host:
|
||||
|
||||
Execute the following command to display your onion host address:
|
||||
|
||||
```sh
|
||||
cat /var/lib/tor/simplex-smp/hostname
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To see which options are available, execute `smp-server` without flags:
|
||||
|
||||
+9
-106
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Hosting your own XFTP Server
|
||||
revision: 31.07.2023
|
||||
revision: 21.04.2023
|
||||
---
|
||||
# Hosting your own XFTP Server
|
||||
|
||||
@@ -17,43 +17,26 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba
|
||||
|
||||
## Installation
|
||||
|
||||
0. First, install `xftp-server`:
|
||||
1. Download `xftp-server` binary:
|
||||
|
||||
- Manual deployment (see below)
|
||||
```sh
|
||||
sudo curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server && sudo chmod +x /usr/local/bin/xftp-server
|
||||
```
|
||||
|
||||
- Semi-automatic deployment:
|
||||
- [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script)
|
||||
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
|
||||
|
||||
Manual installation requires some preliminary actions:
|
||||
|
||||
0. Install binary:
|
||||
|
||||
- Using offical binaries:
|
||||
|
||||
```sh
|
||||
curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server
|
||||
```
|
||||
|
||||
- Compiling from source:
|
||||
|
||||
Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution)
|
||||
|
||||
|
||||
1. Create user and group for `xftp-server`:
|
||||
2. Create user and group for `xftp-server`:
|
||||
|
||||
```sh
|
||||
sudo useradd -m xftp
|
||||
```
|
||||
|
||||
2. Create necessary directories and assign permissions:
|
||||
3. Create necessary directories and assign permissions:
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp
|
||||
sudo chown xftp:xftp /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp
|
||||
```
|
||||
|
||||
3. Allow xftp-server port in firewall:
|
||||
4. Allow xftp-server port in firewall:
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
@@ -63,7 +46,7 @@ Manual installation requires some preliminary actions:
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content:
|
||||
5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content:
|
||||
|
||||
```sh
|
||||
[Unit]
|
||||
@@ -86,86 +69,6 @@ Manual installation requires some preliminary actions:
|
||||
|
||||
And execute `sudo systemctl daemon-reload`.
|
||||
|
||||
## Tor installation
|
||||
|
||||
xftp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user.
|
||||
|
||||
1. Install tor:
|
||||
|
||||
We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide.
|
||||
|
||||
- Configure offical Tor PPA repository:
|
||||
|
||||
```sh
|
||||
CODENAME="$(lsb_release -c | awk '{print $2}')"
|
||||
echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main
|
||||
deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list
|
||||
```
|
||||
|
||||
- Import repository key:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null
|
||||
```
|
||||
|
||||
- Update repository index:
|
||||
|
||||
```sh
|
||||
apt update
|
||||
```
|
||||
|
||||
- Install `tor` package:
|
||||
|
||||
```sh
|
||||
apt install -y tor deb.torproject.org-keyring
|
||||
```
|
||||
|
||||
2. Configure tor:
|
||||
|
||||
- File configuration:
|
||||
|
||||
Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.):
|
||||
|
||||
```sh
|
||||
vim /etc/tor/torrc
|
||||
```
|
||||
|
||||
And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options.
|
||||
|
||||
```sh
|
||||
# Enable log (otherwise, tor doesn't seemd to deploy onion address)
|
||||
Log notice file /var/log/tor/notices.log
|
||||
# Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside xftp-server and onion address will be displayed in clients, this is totally fine)
|
||||
SOCKSPort 0
|
||||
HiddenServiceNonAnonymousMode 1
|
||||
HiddenServiceSingleHopMode 1
|
||||
# xftp-server hidden service host directory and port mappings
|
||||
HiddenServiceDir /var/lib/tor/simplex-xftp/
|
||||
HiddenServicePort 443 localhost:443
|
||||
```
|
||||
|
||||
- Create directories:
|
||||
|
||||
```sh
|
||||
mkdir /var/lib/tor/simplex-xftp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-xftp/ && chmod 700 /var/lib/tor/simplex-xftp/
|
||||
```
|
||||
|
||||
3. Start tor:
|
||||
|
||||
Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case.
|
||||
|
||||
```sh
|
||||
systemctl enable tor && systemctl start tor && systemctl restart tor
|
||||
```
|
||||
|
||||
4. Display onion host:
|
||||
|
||||
Execute the following command to display your onion host address:
|
||||
|
||||
```sh
|
||||
cat /var/lib/tor/simplex-xftp/hostname
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To see which options are available, execute `xftp-server` without flags:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.3.0.5
|
||||
version: 5.3.0.2
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."cf2a17b80ce5736a8b3b02016e3f466f781f259d" = "0yq7kaidnlv9rxl080jv89p8awap046flqzglb71kwy1h1klvyri";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."82aec2cd8f7b4033dbf08d5de33ced216f574bbb" = "1x3rjq10d3c8qb6wf66a2j127xi9xdg21pyw5r4n124f8yvlb0nc";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
|
||||
+2
-3
@@ -5,12 +5,12 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.3.0.5
|
||||
version: 5.3.0.2
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020-23 simplex.chat
|
||||
copyright: 2020-22 simplex.chat
|
||||
license: AGPL-3
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
@@ -107,7 +107,6 @@ library
|
||||
Simplex.Chat.Migrations.M20230621_chat_item_moderations
|
||||
Simplex.Chat.Migrations.M20230705_delivery_receipts
|
||||
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
Simplex.Chat.Migrations.M20230814_indexes
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.WebRTC
|
||||
Simplex.Chat.Options
|
||||
|
||||
+8
-23
@@ -48,7 +48,7 @@ import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.System (SystemTime, systemToUTCTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Simplex.Chat.Archive
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -74,13 +74,11 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||
import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb)
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||
import Simplex.Messaging.Agent as Agent
|
||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), agentClientStore, temporaryAgentError)
|
||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), temporaryAgentError)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -399,7 +397,7 @@ processChatCommand = \case
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user'')
|
||||
pure $ CRActiveUser user''
|
||||
SetActiveUser uName viewPwd_ -> do
|
||||
tryChatError (withStore (`getUserIdByName` uName)) >>= \case
|
||||
tryError (withStore (`getUserIdByName` uName)) >>= \case
|
||||
Left _ -> throwChatError CEUserUnknown
|
||||
Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_
|
||||
SetAllContactReceipts onOff -> withUser $ \_ -> withStore' (`updateAllContactReceipts` onOff) >> ok_
|
||||
@@ -493,18 +491,6 @@ processChatCommand = \case
|
||||
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
|
||||
ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query)
|
||||
ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query)
|
||||
SlowSQLQueries -> do
|
||||
ChatController {chatStore, smpAgent} <- ask
|
||||
chatQueries <- slowQueries chatStore
|
||||
agentQueries <- slowQueries $ agentClientStore smpAgent
|
||||
pure CRSlowSQLQueries {chatQueries, agentQueries}
|
||||
where
|
||||
slowQueries st =
|
||||
liftIO $
|
||||
map (uncurry SlowSQLQuery . first SQL.fromQuery)
|
||||
. sortOn (timeAvg . snd)
|
||||
. M.assocs
|
||||
<$> withConnection st (readTVarIO . DB.slow)
|
||||
APIGetChats userId withPCC -> withUserId userId $ \user ->
|
||||
CRApiChats user <$> withStoreCtx' (Just "APIGetChats, getChatPreviews") (\db -> getChatPreviews db user withPCC)
|
||||
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
|
||||
@@ -1720,7 +1706,7 @@ processChatCommand = \case
|
||||
QuitChat -> liftIO exitSuccess
|
||||
ShowVersion -> do
|
||||
let versionInfo = coreVersionInfo $(simplexmqCommitQ)
|
||||
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
|
||||
chatMigrations <- map upMigration <$> withStore' Migrations.getCurrent
|
||||
agentMigrations <- withAgent getAgentMigrations
|
||||
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
|
||||
DebugLocks -> do
|
||||
@@ -1973,7 +1959,7 @@ processChatCommand = \case
|
||||
drgRandomBytes n = asks idsDrg >>= liftIO . (`randomBytes` n)
|
||||
privateGetUser :: UserId -> m User
|
||||
privateGetUser userId =
|
||||
tryChatError (withStore (`getUser` userId)) >>= \case
|
||||
tryError (withStore (`getUser` userId)) >>= \case
|
||||
Left _ -> throwChatError CEUserUnknown
|
||||
Right user -> pure user
|
||||
validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
|
||||
@@ -3988,7 +3974,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content
|
||||
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
|
||||
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
|
||||
toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole}
|
||||
toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole}
|
||||
whenContactNtfs user ct $
|
||||
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
|
||||
where
|
||||
@@ -5054,7 +5040,6 @@ chatCommandP =
|
||||
"/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP),
|
||||
"/sql chat " *> (ExecChatStoreSQL <$> textP),
|
||||
"/sql agent " *> (ExecAgentStoreSQL <$> textP),
|
||||
"/sql slow" $> SlowSQLQueries,
|
||||
"/_get chats " *> (APIGetChats <$> A.decimal <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)),
|
||||
"/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)),
|
||||
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)),
|
||||
@@ -5156,7 +5141,7 @@ chatCommandP =
|
||||
("/help" <|> "/h") $> ChatHelp HSMain,
|
||||
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
|
||||
"/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP),
|
||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)),
|
||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRAdmin)),
|
||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
|
||||
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
|
||||
@@ -60,7 +60,6 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors)
|
||||
@@ -230,7 +229,6 @@ data ChatCommand
|
||||
| APIStorageEncryption DBEncryptionConfig
|
||||
| ExecChatStoreSQL Text
|
||||
| ExecAgentStoreSQL Text
|
||||
| SlowSQLQueries
|
||||
| APIGetChats {userId :: UserId, pendingConnections :: Bool}
|
||||
| APIGetChat ChatRef ChatPagination (Maybe String)
|
||||
| APIGetChatItems ChatPagination (Maybe String)
|
||||
@@ -565,7 +563,6 @@ data ChatResponse
|
||||
| CRNewContactConnection {user :: User, connection :: PendingContactConnection}
|
||||
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
||||
| CRSQLResult {rows :: [Text]}
|
||||
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
|
||||
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
|
||||
| CRAgentStats {agentStats :: [[String]]}
|
||||
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||
@@ -804,14 +801,6 @@ data SendFileMode
|
||||
| SendFileXFTP
|
||||
deriving (Show, Generic)
|
||||
|
||||
data SlowSQLQuery = SlowSQLQuery
|
||||
{ query :: Text,
|
||||
queryStats :: SlowQueryStats
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON SlowSQLQuery where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data ChatError
|
||||
= ChatError {errorType :: ChatErrorType}
|
||||
| ChatErrorAgent {agentError :: AgentErrorType, connectionEntity_ :: Maybe ConnectionEntity}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230814_indexes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230814_indexes :: Query
|
||||
m20230814_indexes =
|
||||
[sql|
|
||||
CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(user_id, item_status);
|
||||
|]
|
||||
|
||||
down_m20230814_indexes :: Query
|
||||
down_m20230814_indexes =
|
||||
[sql|
|
||||
DROP INDEX idx_chat_items_user_id_item_status;
|
||||
|]
|
||||
@@ -701,7 +701,3 @@ CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses
|
||||
CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses(
|
||||
group_member_id
|
||||
);
|
||||
CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(
|
||||
user_id,
|
||||
item_status
|
||||
);
|
||||
|
||||
@@ -30,7 +30,6 @@ import Foreign.C.Types (CInt (..))
|
||||
import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (poke)
|
||||
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Controller
|
||||
@@ -48,7 +47,6 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..))
|
||||
import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8)
|
||||
import System.IO (utf8)
|
||||
import System.Timeout (timeout)
|
||||
|
||||
foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
@@ -72,12 +70,6 @@ foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Wo
|
||||
-- | check / migrate database and initialize chat controller on success
|
||||
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
cChatMigrateInit fp key conf ctrl = do
|
||||
-- ensure we are set to UTF-8; iOS does not have locale, and will default to
|
||||
-- US-ASCII all the time.
|
||||
setLocaleEncoding utf8
|
||||
setFileSystemEncoding utf8
|
||||
setForeignEncoding utf8
|
||||
|
||||
dbPath <- peekCAString fp
|
||||
dbKey <- peekCAString key
|
||||
confirm <- peekCAString conf
|
||||
|
||||
@@ -16,6 +16,7 @@ import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime (..))
|
||||
import Database.SQLite.Simple ((:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Store.Files
|
||||
import Simplex.Chat.Store.Groups
|
||||
@@ -24,7 +25,6 @@ import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow')
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
|
||||
getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
|
||||
getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
|
||||
@@ -68,13 +68,13 @@ import Data.Maybe (fromMaybe, isJust, isNothing)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
|
||||
getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection
|
||||
getPendingContactConnection db userId connId = do
|
||||
|
||||
@@ -83,6 +83,7 @@ import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay)
|
||||
import Data.Type.Equality
|
||||
import Database.SQLite.Simple (Only (..), (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Store.Direct
|
||||
import Simplex.Chat.Store.Messages
|
||||
@@ -95,7 +96,6 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (week)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
|
||||
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
||||
getLiveSndFileTransfers db User {userId} = do
|
||||
|
||||
@@ -94,6 +94,7 @@ import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Store.Direct
|
||||
@@ -102,7 +103,6 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Util (eitherToMaybe)
|
||||
import UnliftIO.STM
|
||||
@@ -242,11 +242,11 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
|
||||
LEFT JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.user_id = ? AND cc.group_member_id = m.group_member_id
|
||||
where cc.group_member_id = m.group_member_id
|
||||
)
|
||||
WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ?
|
||||
|]
|
||||
(userId, groupMemberId, userId, userContactId)
|
||||
(groupMemberId, userId, userContactId)
|
||||
where
|
||||
toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember)
|
||||
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
||||
@@ -530,7 +530,7 @@ groupMemberQuery =
|
||||
LEFT JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.user_id = ? AND cc.group_member_id = m.group_member_id
|
||||
where cc.group_member_id = m.group_member_id
|
||||
)
|
||||
|]
|
||||
|
||||
@@ -540,7 +540,7 @@ getGroupMember db user@User {userId} groupId groupMemberId =
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?")
|
||||
(userId, groupId, groupMemberId, userId)
|
||||
(groupId, groupMemberId, userId)
|
||||
|
||||
getGroupMemberById :: DB.Connection -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||
getGroupMemberById db user@User {userId} groupMemberId =
|
||||
@@ -548,7 +548,7 @@ getGroupMemberById db user@User {userId} groupMemberId =
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?")
|
||||
(userId, groupMemberId, userId)
|
||||
(groupMemberId, userId)
|
||||
|
||||
getGroupMembers :: DB.Connection -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
@@ -556,7 +556,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
|
||||
(userId, groupId, userId, userContactId)
|
||||
(groupId, userId, userContactId)
|
||||
|
||||
getGroupMembersForExpiration :: DB.Connection -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
@@ -572,7 +572,7 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
|
||||
)
|
||||
|]
|
||||
)
|
||||
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
|
||||
(groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
|
||||
|
||||
toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
|
||||
toContactMember User {userContactId} (memberRow :. connRow) =
|
||||
@@ -998,11 +998,11 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
||||
LEFT JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.user_id = ? AND cc.group_member_id = m.group_member_id
|
||||
where cc.group_member_id = m.group_member_id
|
||||
)
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, userId, contactId, userContactId)
|
||||
(userId, contactId, userContactId)
|
||||
where
|
||||
toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember)
|
||||
toGroupAndMember (groupInfoRow :. memberRow :. connRow) =
|
||||
@@ -1296,11 +1296,11 @@ getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do
|
||||
LEFT JOIN connections ch ON ch.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.user_id = ? AND cc.group_member_id = mh.group_member_id
|
||||
where cc.group_member_id = mh.group_member_id
|
||||
)
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = 0 AND mh.member_category = ?
|
||||
|]
|
||||
(userId, userId, contactId, GCHostMember)
|
||||
(userId, contactId, GCHostMember)
|
||||
where
|
||||
toCont :: (Int64, GroupId, GroupMemberId, MemberId, Maybe ConnReqInvitation) -> Maybe (Int64, XGrpMemIntroCont)
|
||||
toCont (hostConnId, groupId, groupMemberId, memberId, connReq_) = case connReq_ of
|
||||
@@ -1326,11 +1326,11 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
|
||||
LEFT JOIN connections ch ON ch.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.user_id = ? AND cc.group_member_id = mh.group_member_id
|
||||
where cc.group_member_id = mh.group_member_id
|
||||
)
|
||||
WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, userId, groupMemberId, GCHostMember)
|
||||
(userId, groupMemberId, GCHostMember)
|
||||
where
|
||||
toCont :: (Int64, Maybe ConnReqInvitation) -> Maybe (Int64, ConnReqInvitation)
|
||||
toCont (hostConnId, connReq_) = case connReq_ of
|
||||
|
||||
@@ -110,6 +110,7 @@ import Data.Text (Text)
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Messages
|
||||
@@ -121,7 +122,6 @@ import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Util (eitherToMaybe)
|
||||
import UnliftIO.STM
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ import Simplex.Chat.Migrations.M20230618_favorite_chats
|
||||
import Simplex.Chat.Migrations.M20230621_chat_item_moderations
|
||||
import Simplex.Chat.Migrations.M20230705_delivery_receipts
|
||||
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
import Simplex.Chat.Migrations.M20230814_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -150,8 +149,7 @@ schemaMigrations =
|
||||
("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats),
|
||||
("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations),
|
||||
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
|
||||
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses),
|
||||
("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes)
|
||||
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -66,6 +66,7 @@ import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Call
|
||||
@@ -77,7 +78,6 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..))
|
||||
|
||||
@@ -25,7 +25,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query, SQLError, (:.) (..))
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Messages
|
||||
@@ -34,7 +34,6 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Util (allFinally)
|
||||
import UnliftIO.STM
|
||||
@@ -108,7 +107,7 @@ checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left
|
||||
|
||||
handleSQLError :: StoreError -> SQLError -> StoreError
|
||||
handleSQLError err e
|
||||
| SQL.sqlError e == SQL.ErrorConstraint = err
|
||||
| DB.sqlError e == DB.ErrorConstraint = err
|
||||
| otherwise = SEInternalError $ show e
|
||||
|
||||
storeFinally :: ExceptT StoreError IO a -> ExceptT StoreError IO b -> ExceptT StoreError IO a
|
||||
@@ -310,7 +309,7 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate
|
||||
E.try (insertName ldn currentTs) >>= \case
|
||||
Right () -> action ldn
|
||||
Left e
|
||||
| SQL.sqlError e == SQL.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1)
|
||||
| DB.sqlError e == DB.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1)
|
||||
| otherwise -> E.throwIO e
|
||||
where
|
||||
insertName ldn ts =
|
||||
@@ -336,7 +335,7 @@ createWithRandomBytes size gVar create = tryCreate 3
|
||||
liftIO (E.try $ create id') >>= \case
|
||||
Right x -> pure x
|
||||
Left e
|
||||
| SQL.sqlError e == SQL.ErrorConstraint -> tryCreate (n - 1)
|
||||
| DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1)
|
||||
| otherwise -> throwError . SEInternalError $ show e
|
||||
|
||||
encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString
|
||||
|
||||
@@ -25,7 +25,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Database.SQLite.Simple (Only (..))
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import GHC.Weak (deRefWeak)
|
||||
import Simplex.Chat
|
||||
@@ -36,7 +36,6 @@ import Simplex.Chat.Styled
|
||||
import Simplex.Chat.Terminal.Output
|
||||
import Simplex.Chat.Types (User (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Util (catchAll_, safeDecodeUtf8, whenM)
|
||||
import System.Exit (exitSuccess)
|
||||
import System.Terminal hiding (insertChars)
|
||||
@@ -300,7 +299,7 @@ updateTermState user_ st ac live tw (key, ms) ts@TerminalState {inputString = s,
|
||||
getNameSfxs table pfx =
|
||||
getNameSfxs_ pfx (userId, pfx <> "%") $
|
||||
"SELECT local_display_name FROM " <> table <> " WHERE user_id = ? AND local_display_name LIKE ?"
|
||||
getNameSfxs_ :: SQL.ToRow p => Text -> p -> SQL.Query -> IO [String]
|
||||
getNameSfxs_ :: DB.ToRow p => Text -> p -> DB.Query -> IO [String]
|
||||
getNameSfxs_ pfx ps q =
|
||||
withTransaction st (\db -> hasPfx pfx . map fromOnly <$> DB.query db q ps) `catchAll_` pure []
|
||||
commands =
|
||||
|
||||
@@ -47,7 +47,6 @@ import qualified Simplex.FileTransfer.Protocol as XFTP
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -248,13 +247,6 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
||||
CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)]
|
||||
CRNtfMessages {} -> []
|
||||
CRSQLResult rows -> map plain rows
|
||||
CRSlowSQLQueries {chatQueries, agentQueries} ->
|
||||
let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} =
|
||||
"count: " <> sShow count
|
||||
<> (" :: max: " <> sShow timeMax <> " ms")
|
||||
<> (" :: avg: " <> sShow timeAvg <> " ms")
|
||||
<> (" :: " <> plain (T.unwords $ T.lines query))
|
||||
in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries)
|
||||
CRDebugLocks {chatLockName, agentLocks} ->
|
||||
[ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName,
|
||||
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: cf2a17b80ce5736a8b3b02016e3f466f781f259d
|
||||
commit: 82aec2cd8f7b4033dbf08d5de33ced216f574bbb
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
# - ../direct-sqlcipher
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user