mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into ep/donate-page
This commit is contained in:
@@ -871,24 +871,6 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
|
||||
if case let .group(groupInfo) = self.chatInfo {
|
||||
let p = groupInfo.fullGroupPreferences
|
||||
return switch feature {
|
||||
case .timedMessages: p.timedMessages.on
|
||||
case .directMessages: p.directMessages.on(for: groupInfo.membership)
|
||||
case .fullDelete: p.fullDelete.on
|
||||
case .reactions: p.reactions.on
|
||||
case .voice: p.voice.on(for: groupInfo.membership)
|
||||
case .files: p.files.on(for: groupInfo.membership)
|
||||
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
|
||||
case .history: p.history.on
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ struct ChatItemForwardingView: View {
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocused
|
||||
@State private var alert: SomeAlert?
|
||||
@State private var hasSimplexLink_: Bool?
|
||||
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
|
||||
|
||||
var body: some View {
|
||||
@@ -67,34 +66,6 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func prohibitedByPref(_ chat: Chat) -> Bool {
|
||||
// preference checks should match checks in compose view
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files)
|
||||
let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice)
|
||||
return switch chat.chatInfo {
|
||||
case .direct: voiceProhibited
|
||||
case .group: simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||
case .local: false
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
private var hasSimplexLink: Bool {
|
||||
if let hasSimplexLink_ { return hasSimplexLink_ }
|
||||
let r =
|
||||
if let mcText = ci.content.msgContent?.text,
|
||||
let parsedMsg = parseSimpleXMarkdown(mcText) {
|
||||
parsedMsgHasSimplexLink(parsedMsg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
hasSimplexLink_ = r
|
||||
return r
|
||||
}
|
||||
|
||||
private func emptyList() -> some View {
|
||||
Text("No filtered chats")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
@@ -102,7 +73,11 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
|
||||
let prohibited = prohibitedByPref(chat)
|
||||
let prohibited = chat.prohibitedByPref(
|
||||
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
|
||||
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
|
||||
isVoice: ci.content.msgContent?.isVoice ?? false
|
||||
)
|
||||
Button {
|
||||
if prohibited {
|
||||
alert = SomeAlert(
|
||||
|
||||
@@ -43,6 +43,9 @@ struct ChatView: View {
|
||||
@State private var showGroupLinkSheet: Bool = false
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
@State private var selectedChatItems: Set<Int64>? = nil
|
||||
@State private var showDeleteSelectedMessages: Bool = false
|
||||
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -80,25 +83,58 @@ struct ChatView: View {
|
||||
floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
|
||||
}
|
||||
connectingText()
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.disabled(!cInfo.sendMsgEnabled)
|
||||
if selectedChatItems == nil {
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.disabled(!cInfo.sendMsgEnabled)
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
chatItems: ItemsModel.shared.reversedChatItems,
|
||||
selectedChatItems: $selectedChatItems,
|
||||
chatInfo: chat.chatInfo,
|
||||
deleteItems: { forAll in
|
||||
allowToDeleteSelectedMessagesForAll = forAll
|
||||
showDeleteSelectedMessages = true
|
||||
},
|
||||
moderateItems: {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
showModerateSelectedMessagesAlert(groupInfo)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.environmentObject(theme)
|
||||
.confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
if let selected = selectedChatItems {
|
||||
deleteMessages(chat, selected.sorted(), .cidmInternal, moderate: false, deletedSelectedMessages) }
|
||||
}
|
||||
if allowToDeleteSelectedMessagesForAll {
|
||||
Button(broadcastDeleteButtonText(chat), role: .destructive) {
|
||||
if let selected = selectedChatItems {
|
||||
allowToDeleteSelectedMessagesForAll = false
|
||||
deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: false, deletedSelectedMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadChat(chat: chat)
|
||||
initChatView()
|
||||
selectedChatItems = nil
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { cId in
|
||||
showChatInfoSheet = false
|
||||
stopAudioPlayer()
|
||||
if let cId {
|
||||
selectedChatItems = nil
|
||||
if let c = chatModel.getChat(cId) {
|
||||
chat = c
|
||||
}
|
||||
@@ -138,7 +174,9 @@ struct ChatView: View {
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if case let .direct(contact) = cInfo {
|
||||
if selectedChatItems != nil {
|
||||
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
||||
} else if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
@@ -192,66 +230,76 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
if selectedChatItems != nil {
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedChatItems = nil
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
} else {
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
.appSheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
case let .group(groupInfo):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
.appSheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
case .local:
|
||||
searchButton()
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
case .local:
|
||||
searchButton()
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,6 +601,33 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showModerateSelectedMessagesAlert(_ groupInfo: GroupInfo) {
|
||||
guard let count = selectedChatItems?.count, count > 0 else { return }
|
||||
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text(count == 1 ? "Delete member message?" : "Delete \(count) messages of members?"),
|
||||
message: Text(
|
||||
groupInfo.fullGroupPreferences.fullDelete.on
|
||||
? (count == 1 ? "The message will be deleted for all members." : "The messages will be deleted for all members.")
|
||||
: (count == 1 ? "The message will be marked as moderated for all members." : "The messages will be marked as moderated for all members.")
|
||||
),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
if let selected = selectedChatItems {
|
||||
deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: true, deletedSelectedMessages)
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
|
||||
private func deletedSelectedMessages() async {
|
||||
await MainActor.run {
|
||||
withAnimation {
|
||||
selectedChatItems = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadChatItems(_ cInfo: ChatInfo) {
|
||||
Task {
|
||||
if loadingItems || firstPage { return }
|
||||
@@ -604,7 +679,8 @@ struct ChatView: View {
|
||||
maxWidth: maxWidth,
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember,
|
||||
revealedChatItem: $revealedChatItem
|
||||
revealedChatItem: $revealedChatItem,
|
||||
selectedChatItems: $selectedChatItems
|
||||
)
|
||||
}
|
||||
|
||||
@@ -626,6 +702,8 @@ struct ChatView: View {
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showForwardingSheet: Bool = false
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
var revealed: Bool { chatItem == revealedChatItem }
|
||||
@@ -642,9 +720,29 @@ struct ChatView: View {
|
||||
ForEach(items, id: \.1.viewId) { (i, ci) in
|
||||
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
||||
chatItemView(ci, nil, prev)
|
||||
.overlay {
|
||||
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
let checked = selected.contains(ci.id)
|
||||
selectUnselectChatItem(select: !checked, ci)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chatItemView(chatItem, range, prevItem)
|
||||
.overlay {
|
||||
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
let checked = selected.contains(chatItem.id)
|
||||
selectUnselectChatItem(select: !checked, chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -689,11 +787,11 @@ struct ChatView: View {
|
||||
if case let .groupRcv(member) = ci.chatDir,
|
||||
case let .group(groupInfo) = chat.chatInfo {
|
||||
let (prevMember, memCount): (GroupMember?, Int) =
|
||||
if let range = range {
|
||||
m.getPrevHiddenMember(member, range)
|
||||
} else {
|
||||
(nil, 1)
|
||||
}
|
||||
if let range = range {
|
||||
m.getPrevHiddenMember(member, range)
|
||||
} else {
|
||||
(nil, 1)
|
||||
}
|
||||
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
@@ -706,41 +804,64 @@ struct ChatView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.padding(.leading, memberImageSize + 14)
|
||||
.padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0))
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if m.membersLoaded {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
} else {
|
||||
Task {
|
||||
await m.loadGroupMembers(groupInfo) {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
if selectedChatItems != nil && ci.canBeDeletedForSelf {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if m.membersLoaded {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
} else {
|
||||
Task {
|
||||
await m.loadGroupMembers(groupInfo) {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true)
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true)
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, memberImageSize + 8 + 12)
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
if selectedChatItems != nil && ci.canBeDeletedForSelf {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.leading, 12)
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, memberImageSize + 8 + 12)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
} else {
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 5)
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
if selectedChatItems != nil && ci.canBeDeletedForSelf {
|
||||
if chat.chatInfo.chatType == .group {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.leading)
|
||||
}
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,17 +896,17 @@ struct ChatView: View {
|
||||
}
|
||||
.confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
deleteMessage(.cidmInternal)
|
||||
deleteMessage(.cidmInternal, moderate: false)
|
||||
}
|
||||
if let di = deletingItem, di.meta.deletable && !di.localNote {
|
||||
Button(broadcastDeleteButtonText, role: .destructive) {
|
||||
deleteMessage(.cidmBroadcast)
|
||||
Button(broadcastDeleteButtonText(chat), role: .destructive) {
|
||||
deleteMessage(.cidmBroadcast, moderate: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
deleteMessages()
|
||||
deleteMessages(chat, deletingItems, moderate: false)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
@@ -894,7 +1015,7 @@ struct ChatView: View {
|
||||
if !live || !ci.meta.isLive {
|
||||
deleteButton(ci)
|
||||
}
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo), ci.chatDir != .groupSnd {
|
||||
moderateButton(ci, groupInfo)
|
||||
}
|
||||
} else if ci.meta.itemDeleted != nil {
|
||||
@@ -918,6 +1039,10 @@ struct ChatView: View {
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
if selectedChatItems == nil && ci.canBeDeletedForSelf {
|
||||
Divider()
|
||||
selectButton(ci)
|
||||
}
|
||||
}
|
||||
|
||||
var replyButton: Button<some View> {
|
||||
@@ -1090,6 +1215,21 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func selectButton(_ ci: ChatItem) -> Button<some View> {
|
||||
Button {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
withAnimation {
|
||||
selectUnselectChatItem(select: true, ci)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Select", comment: "chat item action"),
|
||||
systemImage: "checkmark.circle"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func viewInfoButton(_ ci: ChatItem) -> Button<some View> {
|
||||
Button {
|
||||
Task {
|
||||
@@ -1200,7 +1340,7 @@ struct ChatView: View {
|
||||
),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deletingItem = ci
|
||||
deleteMessage(.cidmBroadcast)
|
||||
deleteMessage(.cidmBroadcast, moderate: true)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
@@ -1251,47 +1391,46 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var broadcastDeleteButtonText: LocalizedStringKey {
|
||||
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
|
||||
}
|
||||
|
||||
var deleteMessagesTitle: LocalizedStringKey {
|
||||
let n = deletingItems.count
|
||||
return n == 1 ? "Delete message?" : "Delete \(n) messages?"
|
||||
}
|
||||
|
||||
private func deleteMessages() {
|
||||
let itemIds = deletingItems
|
||||
if itemIds.count > 0 {
|
||||
let chatInfo = chat.chatInfo
|
||||
Task {
|
||||
do {
|
||||
let deletedItems = try await apiDeleteChatItems(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
itemIds: itemIds,
|
||||
mode: .cidmInternal
|
||||
)
|
||||
await MainActor.run {
|
||||
for di in deletedItems {
|
||||
m.removeChatItem(chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("ChatView.deleteMessage error: \(error.localizedDescription)")
|
||||
private func selectUnselectChatItem(select: Bool, _ ci: ChatItem) {
|
||||
selectedChatItems = selectedChatItems ?? []
|
||||
var itemIds: [Int64] = []
|
||||
if !revealed,
|
||||
let currIndex = m.getChatItemIndex(ci),
|
||||
let ciCategory = ci.mergeCategory {
|
||||
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||
if let range = itemsRange(currIndex, prevHidden) {
|
||||
for i in range {
|
||||
itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
|
||||
}
|
||||
} else {
|
||||
itemIds.append(ci.id)
|
||||
}
|
||||
} else {
|
||||
itemIds.append(ci.id)
|
||||
}
|
||||
if select {
|
||||
if let sel = selectedChatItems {
|
||||
selectedChatItems = sel.union(itemIds)
|
||||
}
|
||||
} else {
|
||||
itemIds.forEach { selectedChatItems?.remove($0) }
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteMessage(_ mode: CIDeleteMode) {
|
||||
private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) {
|
||||
logger.debug("ChatView deleteMessage")
|
||||
Task {
|
||||
logger.debug("ChatView deleteMessage: in Task")
|
||||
do {
|
||||
if let di = deletingItem {
|
||||
let r = if case .cidmBroadcast = mode,
|
||||
let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) {
|
||||
moderate,
|
||||
let (groupInfo, _) = di.memberToModerate(chat.chatInfo) {
|
||||
try await apiDeleteMemberChatItems(
|
||||
groupId: groupInfo.apiId,
|
||||
itemIds: [di.id]
|
||||
@@ -1320,6 +1459,68 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SelectedChatItem: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var ciId: Int64
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
@State var checked: Bool = false
|
||||
var body: some View {
|
||||
Image(systemName: checked ? "checkmark.circle.fill" : "circle")
|
||||
.resizable()
|
||||
.foregroundColor(checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel))
|
||||
.frame(width: 24, height: 24)
|
||||
.onAppear {
|
||||
checked = selectedChatItems?.contains(ciId) == true
|
||||
}
|
||||
.onChange(of: selectedChatItems) { selected in
|
||||
checked = selected?.contains(ciId) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
|
||||
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
|
||||
}
|
||||
|
||||
private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDeleteMode = .cidmInternal, moderate: Bool, _ onSuccess: @escaping () async -> Void = {}) {
|
||||
let itemIds = deletingItems
|
||||
if itemIds.count > 0 {
|
||||
let chatInfo = chat.chatInfo
|
||||
Task {
|
||||
do {
|
||||
let deletedItems = if case .cidmBroadcast = mode,
|
||||
moderate,
|
||||
case .group = chat.chatInfo {
|
||||
try await apiDeleteMemberChatItems(
|
||||
groupId: chatInfo.apiId,
|
||||
itemIds: itemIds
|
||||
)
|
||||
} else {
|
||||
try await apiDeleteChatItems(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
itemIds: itemIds,
|
||||
mode: mode
|
||||
)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
for di in deletedItems {
|
||||
if let toItem = di.toChatItem {
|
||||
_ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem)
|
||||
} else {
|
||||
ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
await onSuccess()
|
||||
} catch {
|
||||
logger.error("ChatView.deleteMessages error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1120,10 +1120,6 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
|
||||
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
}
|
||||
|
||||
struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// SelectableChatItemToolbars.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Stanislav Dmitrenko on 30.07.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SelectedItemsTopToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
|
||||
var body: some View {
|
||||
let count = selectedChatItems?.count ?? 0
|
||||
return Text(count == 0 ? "Nothing selected" : "Selected \(count)").font(.headline)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.frame(width: 220)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectedItemsBottomToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let chatItems: [ChatItem]
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
var chatInfo: ChatInfo
|
||||
// Bool - delete for everyone is possible
|
||||
var deleteItems: (Bool) -> Void
|
||||
var moderateItems: () -> Void
|
||||
//var shareItems: () -> Void
|
||||
@State var deleteEnabled: Bool = false
|
||||
@State var deleteForEveryoneEnabled: Bool = false
|
||||
|
||||
@State var canModerate: Bool = false
|
||||
@State var moderateEnabled: Bool = false
|
||||
|
||||
@State var allButtonsDisabled = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button {
|
||||
deleteItems(deleteForEveryoneEnabled)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red)
|
||||
}
|
||||
.disabled(!deleteEnabled || allButtonsDisabled)
|
||||
|
||||
Spacer()
|
||||
Button {
|
||||
moderateItems()
|
||||
} label: {
|
||||
Image(systemName: "flag")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red)
|
||||
}
|
||||
.disabled(!moderateEnabled || allButtonsDisabled)
|
||||
.opacity(canModerate ? 1 : 0)
|
||||
|
||||
|
||||
Spacer()
|
||||
Button {
|
||||
//shareItems()
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
|
||||
}
|
||||
.disabled(allButtonsDisabled)
|
||||
.opacity(0)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding([.leading, .trailing], 12)
|
||||
}
|
||||
.onAppear {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: chatInfo) { info in
|
||||
recheckItems(info, chatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: chatItems) { items in
|
||||
recheckItems(chatInfo, items, selectedChatItems)
|
||||
}
|
||||
.onChange(of: selectedChatItems) { selected in
|
||||
recheckItems(chatInfo, chatItems, selected)
|
||||
}
|
||||
.frame(height: 55.5)
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set<Int64>?) {
|
||||
let count = selectedItems?.count ?? 0
|
||||
allButtonsDisabled = count == 0 || count > 20
|
||||
canModerate = possibleToModerate(chatInfo)
|
||||
if let selected = selectedItems {
|
||||
(deleteEnabled, deleteForEveryoneEnabled, moderateEnabled, _, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in
|
||||
if selected.contains(ci.id) {
|
||||
var (de, dee, me, onlyOwnGroupItems, sel) = r
|
||||
de = de && ci.canBeDeletedForSelf
|
||||
dee = dee && ci.meta.deletable && !ci.localNote
|
||||
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd
|
||||
me = me && !onlyOwnGroupItems && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil
|
||||
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
return (de, dee, me, onlyOwnGroupItems, sel)
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool {
|
||||
return switch chatInfo {
|
||||
case let .group(groupInfo):
|
||||
groupInfo.membership.memberRole >= .admin
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let defaultProfileImageCorner = 22.5
|
||||
|
||||
struct ProfileImage: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var imageStr: String? = nil
|
||||
@@ -34,26 +32,6 @@ struct ProfileImage: View {
|
||||
}
|
||||
}
|
||||
|
||||
private let squareToCircleRatio = 0.935
|
||||
|
||||
private let radiusFactor = (1 - squareToCircleRatio) / 50
|
||||
|
||||
@ViewBuilder func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View {
|
||||
let v = img.resizable()
|
||||
if radius >= 50 {
|
||||
v.frame(width: size, height: size).clipShape(Circle())
|
||||
} else if radius <= 0 {
|
||||
let sz = size * squareToCircleRatio
|
||||
v.frame(width: sz, height: sz).padding((size - sz) / 2)
|
||||
} else {
|
||||
let sz = size * (squareToCircleRatio + radius * radiusFactor)
|
||||
v.frame(width: sz, height: sz)
|
||||
.clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous))
|
||||
.padding((size - sz) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Color {
|
||||
func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color {
|
||||
return self
|
||||
|
||||
@@ -76,7 +76,7 @@ extension AppSettings {
|
||||
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
||||
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
||||
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||
c.uiProfileImageCornerRadius = def.float(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
c.uiColorScheme = currentThemeDefault.get()
|
||||
c.uiDarkColorScheme = systemDarkThemeDefault.get()
|
||||
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
|
||||
|
||||
@@ -143,7 +143,8 @@ struct AppearanceSettings: View {
|
||||
Text("Themes")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.onChange(of: profileImageCornerRadius) { _ in
|
||||
.onChange(of: profileImageCornerRadius) { cornerRadius in
|
||||
profileImageCornerRadiusGroupDefault.set(cornerRadius)
|
||||
saveThemeToDatabase(nil)
|
||||
}
|
||||
.onChange(of: colorMode) { mode in
|
||||
|
||||
@@ -70,6 +70,9 @@ struct PrivacySettings: View {
|
||||
Section {
|
||||
settingsRow("network", color: theme.colors.secondary) {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
.onChange(of: useLinkPreviews) { linkPreviews in
|
||||
privacyLinkPreviewsGroupDefault.set(linkPreviews)
|
||||
}
|
||||
}
|
||||
settingsRow("message", color: theme.colors.secondary) {
|
||||
Toggle("Show last messages", isOn: $showChatPreviews)
|
||||
@@ -365,6 +368,7 @@ struct SimplexLockView: View {
|
||||
@State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
|
||||
@State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
|
||||
@AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = ""
|
||||
@AppStorage(GROUP_DEFAULT_ALLOW_SHARE_EXTENSION, store: groupDefaults) private var allowShareExtension = false
|
||||
@State private var performLAToggleReset = false
|
||||
@State private var performLAModeReset = false
|
||||
@State private var performLASelfDestructReset = false
|
||||
@@ -436,6 +440,12 @@ struct SimplexLockView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if performLA {
|
||||
Section("Share to SimpleX") {
|
||||
Toggle("Allow sharing", isOn: $allowShareExtension)
|
||||
}
|
||||
}
|
||||
|
||||
if performLA && laMode == .passcode {
|
||||
Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) {
|
||||
Toggle(isOn: $selfDestruct) {
|
||||
@@ -460,6 +470,7 @@ struct SimplexLockView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: performLA) { performLAToggle in
|
||||
performLAGroupDefault.set(performLAToggle)
|
||||
prefLANoticeShown = true
|
||||
if performLAToggleReset {
|
||||
performLAToggleReset = false
|
||||
|
||||
@@ -18,7 +18,7 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as?
|
||||
|
||||
let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice"
|
||||
let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown"
|
||||
let DEFAULT_PERFORM_LA = "performLocalAuthentication"
|
||||
let DEFAULT_PERFORM_LA = "performLocalAuthentication" // deprecated, moved to app group
|
||||
let DEFAULT_LA_MODE = "localAuthenticationMode"
|
||||
let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay"
|
||||
let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct"
|
||||
@@ -28,7 +28,7 @@ let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
||||
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
|
||||
let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
|
||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead
|
||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group
|
||||
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
|
||||
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
||||
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
|
||||
@@ -165,6 +165,9 @@ let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(def
|
||||
|
||||
func setGroupDefaults() {
|
||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||
performLAGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA))
|
||||
privacyLinkPreviewsGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS))
|
||||
profileImageCornerRadiusGroupDefault.set(UserDefaults.standard.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS))
|
||||
}
|
||||
|
||||
public class StringDefault {
|
||||
|
||||
@@ -772,6 +772,10 @@
|
||||
<target>Разреши изпращането на изчезващи съобщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Позволи необратимо изтриване на изпратените съобщения. (24 часа)</target>
|
||||
@@ -1857,6 +1861,10 @@ This is your own one-time link!</source>
|
||||
<target>Изтрий</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Изтриване на %lld съобщения?</target>
|
||||
@@ -4304,6 +4312,10 @@ This is your link for group %@!</source>
|
||||
<target>Несъвместим!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Известия</target>
|
||||
@@ -5441,6 +5453,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Избери</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5790,6 +5806,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Сподели този еднократен линк за връзка</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Сподели с контактите</target>
|
||||
@@ -6243,6 +6263,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>Съобщението ще бъде маркирано като модерирано за всички членове.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Ново поколение поверителни съобщения</target>
|
||||
@@ -8426,10 +8454,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8442,6 +8482,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8450,6 +8494,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8458,10 +8506,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8518,10 +8562,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8530,10 +8582,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8546,6 +8594,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -749,6 +749,10 @@
|
||||
<target>Povolit odesílání mizících zpráv.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Povolit nevratné smazání odeslaných zpráv. (24 hodin)</target>
|
||||
@@ -1787,6 +1791,10 @@ This is your own one-time link!</source>
|
||||
<target>Smazat</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4143,6 +4151,10 @@ This is your link for group %@!</source>
|
||||
<source>Not compatible!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Oznámení</target>
|
||||
@@ -5240,6 +5252,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Vybrat</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5584,6 +5600,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Sdílet s kontakty</target>
|
||||
@@ -6026,6 +6046,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
||||
<target>Zpráva bude pro všechny členy označena jako moderovaná.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Nová generace soukromých zpráv</target>
|
||||
@@ -8123,10 +8151,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8139,6 +8179,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8147,6 +8191,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8155,10 +8203,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8215,10 +8259,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8227,10 +8279,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8243,6 +8291,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Das Senden von verschwindenden Nachrichten erlauben.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)</target>
|
||||
@@ -1894,6 +1898,10 @@ Das ist Ihr eigener Einmal-Link!</target>
|
||||
<target>Löschen</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>%lld Nachrichten löschen?</target>
|
||||
@@ -4387,6 +4395,10 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
||||
<target>Nicht kompatibel!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Benachrichtigungen</target>
|
||||
@@ -5560,6 +5572,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Auswählen</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
||||
<target>Teilen Sie diesen Einmal-Einladungslink</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Mit Kontakten teilen</target>
|
||||
@@ -6392,6 +6412,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
|
||||
<target>Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Die nächste Generation von privatem Messaging</target>
|
||||
@@ -8609,10 +8637,22 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,11 @@
|
||||
<target>Allow sending disappearing messages.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<target>Allow sharing</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Allow to irreversibly delete sent messages. (24 hours)</target>
|
||||
@@ -1896,6 +1901,11 @@ This is your own one-time link!</target>
|
||||
<target>Delete</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<target>Delete %lld messages of members?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Delete %lld messages?</target>
|
||||
@@ -4398,6 +4408,11 @@ This is your link for group %@!</target>
|
||||
<target>Not compatible!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<target>Nothing selected</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Notifications</target>
|
||||
@@ -5571,6 +5586,11 @@ Enable in *Network & servers* settings.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Select</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Selected %lld</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5938,6 +5958,11 @@ Enable in *Network & servers* settings.</target>
|
||||
<target>Share this 1-time invite link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<target>Share to SimpleX</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Share with contacts</target>
|
||||
@@ -6405,6 +6430,16 @@ It can happen because of some bug or when the connection is compromised.</target
|
||||
<target>The message will be marked as moderated for all members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<target>The messages will be deleted for all members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<target>The messages will be marked as moderated for all members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>The next generation of private messaging</target>
|
||||
@@ -8626,11 +8661,26 @@ last received msg: %2$@</target>
|
||||
<target>%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<target>App is locked!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<target>Cancel</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Cannot access keychain to save database password</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<target>Cannot forward message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<target>Comment</target>
|
||||
@@ -8646,6 +8696,11 @@ last received msg: %2$@</target>
|
||||
<target>Database downgrade required</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<target>Database encrypted!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<target>Database error</target>
|
||||
@@ -8656,6 +8711,11 @@ last received msg: %2$@</target>
|
||||
<target>Database passphrase is different from saved in the keychain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<target>Database passphrase is required to open chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<target>Database upgrade required</target>
|
||||
@@ -8666,11 +8726,6 @@ last received msg: %2$@</target>
|
||||
<target>Dismiss Sheet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Encrypted database</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<target>Error preparing file</target>
|
||||
@@ -8741,11 +8796,21 @@ last received msg: %2$@</target>
|
||||
<target>Open the app to upgrade the database.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<target>Passphrase</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<target>Please create a profile in the SimpleX app</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<target>Selected chat preferences prohibit this message.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<target>Sending File</target>
|
||||
@@ -8756,11 +8821,6 @@ last received msg: %2$@</target>
|
||||
<target>Share</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<target>Sharing is not supported when passphrase is not stored in KeyChain.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<target>Unknown database error: %@</target>
|
||||
@@ -8776,6 +8836,11 @@ last received msg: %2$@</target>
|
||||
<target>Wrong database passphrase</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<target>You can allow sharing in Privacy & Security / SimpleX Lock settings.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Permites el envío de mensajes temporales.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Se permite la eliminación irreversible de mensajes. (24 horas)</target>
|
||||
@@ -1894,6 +1898,10 @@ This is your own one-time link!</source>
|
||||
<target>Eliminar</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>¿Elimina %lld mensajes?</target>
|
||||
@@ -4387,6 +4395,10 @@ This is your link for group %@!</source>
|
||||
<target>¡No compatible!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Notificaciones</target>
|
||||
@@ -5560,6 +5572,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Seleccionar</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
<target>Comparte este enlace de un solo uso</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Compartir con contactos</target>
|
||||
@@ -6392,6 +6412,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target>
|
||||
<target>El mensaje será marcado como moderado para todos los miembros.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>La nueva generación de mensajería privada</target>
|
||||
@@ -8609,10 +8637,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -744,6 +744,10 @@
|
||||
<target>Salli katoavien viestien lähettäminen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia)</target>
|
||||
@@ -1780,6 +1784,10 @@ This is your own one-time link!</source>
|
||||
<target>Poista</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4132,6 +4140,10 @@ This is your link for group %@!</source>
|
||||
<source>Not compatible!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Ilmoitukset</target>
|
||||
@@ -5228,6 +5240,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Valitse</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5571,6 +5587,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Jaa kontaktien kanssa</target>
|
||||
@@ -6012,6 +6032,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
|
||||
<target>Viesti merkitään moderoiduksi kaikille jäsenille.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Seuraavan sukupolven yksityisviestit</target>
|
||||
@@ -8107,10 +8135,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8123,6 +8163,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8131,6 +8175,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8139,10 +8187,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8199,10 +8243,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8211,10 +8263,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8227,6 +8275,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Autorise l’envoi de messages éphémères.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Autoriser la suppression irréversible de messages envoyés. (24 heures)</target>
|
||||
@@ -1894,6 +1898,10 @@ Il s'agit de votre propre lien unique !</target>
|
||||
<target>Supprimer</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Supprimer %lld messages ?</target>
|
||||
@@ -4387,6 +4395,10 @@ Voici votre lien pour le groupe %@ !</target>
|
||||
<target>Non compatible !</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Notifications</target>
|
||||
@@ -5560,6 +5572,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Choisir</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
|
||||
<target>Partager ce lien d'invitation unique</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Partager avec vos contacts</target>
|
||||
@@ -6391,6 +6411,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise.
|
||||
<target>Le message sera marqué comme modéré pour tous les membres.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>La nouvelle génération de messagerie privée</target>
|
||||
@@ -8608,10 +8636,22 @@ dernier message reçu : %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8624,6 +8664,10 @@ dernier message reçu : %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8632,6 +8676,10 @@ dernier message reçu : %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8640,10 +8688,6 @@ dernier message reçu : %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8700,10 +8744,18 @@ dernier message reçu : %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8712,10 +8764,6 @@ dernier message reçu : %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8728,6 +8776,10 @@ dernier message reçu : %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Az eltűnő üzenetek küldése engedélyezve van.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Elküldött üzenetek végleges törlésének engedélyezése. (24 óra)</target>
|
||||
@@ -1894,6 +1898,10 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
<target>Törlés</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Töröl %lld üzenetet?</target>
|
||||
@@ -4387,6 +4395,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!</target>
|
||||
<target>Nem kompatibilis!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Értesítések</target>
|
||||
@@ -5560,6 +5572,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.<
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Választás</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.<
|
||||
<target>Egyszer használatos meghívó hivatkozás megosztása</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Megosztás ismerősökkel</target>
|
||||
@@ -6392,6 +6412,14 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.</target>
|
||||
<target>Az üzenet minden tag számára moderáltként lesz megjelölve.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>A privát üzenetküldés következő generációja</target>
|
||||
@@ -8609,10 +8637,22 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ utoljára fogadott üzenet: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Permetti l'invio di messaggi a tempo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)</target>
|
||||
@@ -1894,6 +1898,10 @@ Questo è il tuo link una tantum!</target>
|
||||
<target>Elimina</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Eliminare %lld messaggi?</target>
|
||||
@@ -4387,6 +4395,10 @@ Questo è il tuo link per il gruppo %@!</target>
|
||||
<target>Non compatibile!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Notifiche</target>
|
||||
@@ -5560,6 +5572,10 @@ Attivalo nelle impostazioni *Rete e server*.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Seleziona</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Attivalo nelle impostazioni *Rete e server*.</target>
|
||||
<target>Condividi questo link di invito una tantum</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Condividi con i contatti</target>
|
||||
@@ -6392,6 +6412,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta
|
||||
<target>Il messaggio sarà segnato come moderato per tutti i membri.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>La nuova generazione di messaggistica privata</target>
|
||||
@@ -8609,10 +8637,22 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ ultimo msg ricevuto: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -762,6 +762,10 @@
|
||||
<target>消えるメッセージの送信を許可する。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>送信済みメッセージの永久削除を許可する。(24時間)</target>
|
||||
@@ -1804,6 +1808,10 @@ This is your own one-time link!</source>
|
||||
<target>削除</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4157,6 +4165,10 @@ This is your link for group %@!</source>
|
||||
<source>Not compatible!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>通知</target>
|
||||
@@ -5253,6 +5265,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>選択</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5589,6 +5605,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>連絡先と共有する</target>
|
||||
@@ -6031,6 +6051,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>次世代のプライバシー・メッセンジャー</target>
|
||||
@@ -8125,10 +8153,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8141,6 +8181,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8149,6 +8193,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8157,10 +8205,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8217,10 +8261,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8229,10 +8281,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8245,6 +8293,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Toestaan dat verdwijnende berichten worden verzonden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur)</target>
|
||||
@@ -1894,6 +1898,10 @@ Dit is uw eigen eenmalige link!</target>
|
||||
<target>Verwijderen</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>%lld berichten verwijderen?</target>
|
||||
@@ -4387,6 +4395,10 @@ Dit is jouw link voor groep %@!</target>
|
||||
<target>Niet compatibel!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Meldingen</target>
|
||||
@@ -5560,6 +5572,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Selecteer</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
|
||||
<target>Deel deze eenmalige uitnodigingslink</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Delen met contacten</target>
|
||||
@@ -6392,6 +6412,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target>
|
||||
<target>Het bericht wordt gemarkeerd als gemodereerd voor alle leden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>De volgende generatie privéberichten</target>
|
||||
@@ -8609,10 +8637,22 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ laatst ontvangen bericht: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -783,6 +783,10 @@
|
||||
<target>Zezwól na wysyłanie znikających wiadomości.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)</target>
|
||||
@@ -1894,6 +1898,10 @@ To jest twój jednorazowy link!</target>
|
||||
<target>Usuń</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Usunąć %lld wiadomości?</target>
|
||||
@@ -4387,6 +4395,10 @@ To jest twój link do grupy %@!</target>
|
||||
<target>Nie kompatybilny!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Powiadomienia</target>
|
||||
@@ -5560,6 +5572,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Wybierz</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5927,6 +5943,10 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
<target>Udostępnij ten jednorazowy link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Udostępnij kontaktom</target>
|
||||
@@ -6392,6 +6412,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom
|
||||
<target>Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Następna generacja prywatnych wiadomości</target>
|
||||
@@ -8609,10 +8637,22 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8625,6 +8665,10 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8633,6 +8677,10 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8641,10 +8689,6 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8701,10 +8745,18 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8713,10 +8765,6 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8729,6 +8777,10 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -773,6 +773,10 @@
|
||||
<target>Разрешить посылать исчезающие сообщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Разрешить необратимо удалять отправленные сообщения. (24 часа)</target>
|
||||
@@ -1861,6 +1865,10 @@ This is your own one-time link!</source>
|
||||
<target>Удалить</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Удалить %lld сообщений?</target>
|
||||
@@ -4320,6 +4328,10 @@ This is your link for group %@!</source>
|
||||
<target>Несовместимая версия!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Уведомления</target>
|
||||
@@ -5464,6 +5476,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Выбрать</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5817,6 +5833,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Поделиться одноразовой ссылкой-приглашением</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Поделиться с контактами</target>
|
||||
@@ -6273,6 +6293,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>Сообщение будет помечено как удаленное для всех членов группы.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Новое поколение приватных сообщений</target>
|
||||
@@ -8466,10 +8494,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8482,6 +8522,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8490,6 +8534,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8498,10 +8546,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8558,10 +8602,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8571,10 +8623,6 @@ last received msg: %2$@</source>
|
||||
<target>Поделиться</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8587,6 +8635,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -736,6 +736,10 @@
|
||||
<target>อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target>
|
||||
@@ -1769,6 +1773,10 @@ This is your own one-time link!</source>
|
||||
<target>ลบ</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4113,6 +4121,10 @@ This is your link for group %@!</source>
|
||||
<source>Not compatible!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>การแจ้งเตือน</target>
|
||||
@@ -5207,6 +5219,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>เลือก</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5548,6 +5564,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>แชร์กับผู้ติดต่อ</target>
|
||||
@@ -5988,6 +6008,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>การส่งข้อความส่วนตัวรุ่นต่อไป</target>
|
||||
@@ -8075,10 +8103,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8091,6 +8131,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8099,6 +8143,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8107,10 +8155,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8167,10 +8211,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8179,10 +8231,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8195,6 +8243,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -773,6 +773,10 @@
|
||||
<target>Kendiliğinden yok olan mesajlar göndermeye izin ver.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde)</target>
|
||||
@@ -1862,6 +1866,10 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
|
||||
<target>Sil</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>%lld mesaj silinsin mi?</target>
|
||||
@@ -4322,6 +4330,10 @@ Bu senin grup için bağlantın %@!</target>
|
||||
<target>Uyumlu değil!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Bildirimler</target>
|
||||
@@ -5466,6 +5478,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Seç</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5819,6 +5835,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Bu tek kullanımlık bağlantı davetini paylaş</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Kişilerle paylaş</target>
|
||||
@@ -6275,6 +6295,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir.
|
||||
<target>Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Gizli mesajlaşmanın yeni nesli</target>
|
||||
@@ -8471,10 +8499,22 @@ son alınan msj: %2$@</target>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8487,6 +8527,10 @@ son alınan msj: %2$@</target>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8495,6 +8539,10 @@ son alınan msj: %2$@</target>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8503,10 +8551,6 @@ son alınan msj: %2$@</target>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8563,10 +8607,18 @@ son alınan msj: %2$@</target>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8575,10 +8627,6 @@ son alınan msj: %2$@</target>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8591,6 +8639,10 @@ son alınan msj: %2$@</target>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -773,6 +773,10 @@
|
||||
<target>Дозволити надсилання зникаючих повідомлень.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)</target>
|
||||
@@ -1862,6 +1866,10 @@ This is your own one-time link!</source>
|
||||
<target>Видалити</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target>Видалити %lld повідомлень?</target>
|
||||
@@ -4322,6 +4330,10 @@ This is your link for group %@!</source>
|
||||
<target>Не сумісні!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>Сповіщення</target>
|
||||
@@ -5466,6 +5478,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>Виберіть</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5819,6 +5835,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Поділіться цим одноразовим посиланням-запрошенням</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>Поділіться з контактами</target>
|
||||
@@ -6275,6 +6295,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>Повідомлення буде позначено як модероване для всіх учасників.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>Наступне покоління приватних повідомлень</target>
|
||||
@@ -8471,10 +8499,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8487,6 +8527,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8495,6 +8539,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8503,10 +8551,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8563,10 +8607,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8575,10 +8627,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8591,6 +8639,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -759,6 +759,10 @@
|
||||
<target>允许发送限时消息。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target>允许不可撤回地删除已发送消息。</target>
|
||||
@@ -1835,6 +1839,10 @@ This is your own one-time link!</source>
|
||||
<target>删除</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
<source>Delete %lld messages?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4263,6 +4271,10 @@ This is your link for group %@!</source>
|
||||
<target>不兼容!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
<source>Notifications</source>
|
||||
<target>通知</target>
|
||||
@@ -5391,6 +5403,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>选择</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5740,6 +5756,10 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>分享此一次性邀请链接</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>与联系人分享</target>
|
||||
@@ -6193,6 +6213,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>该消息将对所有成员标记为已被管理员移除。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<target>下一代私密通讯软件</target>
|
||||
@@ -8353,10 +8381,22 @@ last received msg: %2$@</source>
|
||||
<source>%@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8369,6 +8409,10 @@ last received msg: %2$@</source>
|
||||
<source>Database downgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8377,6 +8421,10 @@ last received msg: %2$@</source>
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8385,10 +8433,6 @@ last received msg: %2$@</source>
|
||||
<source>Dismiss Sheet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8445,10 +8489,18 @@ last received msg: %2$@</source>
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending File" xml:space="preserve">
|
||||
<source>Sending File</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8457,10 +8509,6 @@ last received msg: %2$@</source>
|
||||
<source>Share</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sharing is not supported when passphrase is not stored in KeyChain." xml:space="preserve">
|
||||
<source>Sharing is not supported when passphrase is not stored in KeyChain.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -8473,6 +8521,10 @@ last received msg: %2$@</source>
|
||||
<source>Wrong database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -19,14 +19,16 @@ private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000
|
||||
|
||||
class ShareModel: ObservableObject {
|
||||
@Published var sharedContent: SharedContent?
|
||||
@Published var chats = Array<ChatData>()
|
||||
@Published var profileImages = Dictionary<ChatInfo.ID, UIImage>()
|
||||
@Published var search = String()
|
||||
@Published var comment = String()
|
||||
@Published var chats: [ChatData] = []
|
||||
@Published var profileImages: [ChatInfo.ID: UIImage] = [:]
|
||||
@Published var search = ""
|
||||
@Published var comment = ""
|
||||
@Published var selected: ChatData?
|
||||
@Published var isLoaded = false
|
||||
@Published var bottomBar: BottomBar = .loadingSpinner
|
||||
@Published var errorAlert: ErrorAlert?
|
||||
@Published var hasSimplexLink = false
|
||||
@Published var alertRequiresPassword = false
|
||||
|
||||
enum BottomBar {
|
||||
case sendButton
|
||||
@@ -48,9 +50,22 @@ class ShareModel: ObservableObject {
|
||||
|
||||
private var itemProvider: NSItemProvider?
|
||||
|
||||
var isSendDisbled: Bool { sharedContent == nil || selected == nil }
|
||||
var isSendDisbled: Bool { sharedContent == nil || selected == nil || isProhibited(selected) }
|
||||
|
||||
var isLinkPreview: Bool {
|
||||
switch sharedContent {
|
||||
case .url: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var filteredChats: Array<ChatData> {
|
||||
func isProhibited(_ chat: ChatData?) -> Bool {
|
||||
if let chat, let sharedContent {
|
||||
sharedContent.prohibited(in: chat, hasSimplexLink: hasSimplexLink)
|
||||
} else { false }
|
||||
}
|
||||
|
||||
var filteredChats: [ChatData] {
|
||||
search.isEmpty
|
||||
? filterChatsToForwardTo(chats: chats)
|
||||
: filterChatsToForwardTo(chats: chats)
|
||||
@@ -58,6 +73,10 @@ class ShareModel: ObservableObject {
|
||||
}
|
||||
|
||||
func setup(context: NSExtensionContext) {
|
||||
if performLAGroupDefault.get() && !allowShareExtensionGroupDefault.get() {
|
||||
errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.")
|
||||
return
|
||||
}
|
||||
if let item = context.inputItems.first as? NSExtensionItem,
|
||||
let itemProvider = item.attachments?.first {
|
||||
self.itemProvider = itemProvider
|
||||
@@ -67,43 +86,47 @@ class ShareModel: ObservableObject {
|
||||
apiSuspendChat(expired: $0)
|
||||
}
|
||||
}
|
||||
// Init Chat
|
||||
Task {
|
||||
if let e = initChat() {
|
||||
await MainActor.run { errorAlert = e }
|
||||
} else {
|
||||
// Load Chats
|
||||
Task {
|
||||
switch fetchChats() {
|
||||
case let .success(chats):
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = UIImage(base64Encoded: profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
setup()
|
||||
}
|
||||
}
|
||||
|
||||
func setup(with dbKey: String? = nil) {
|
||||
// Init Chat
|
||||
Task {
|
||||
if let e = initChat(with: dbKey) {
|
||||
await MainActor.run { errorAlert = e }
|
||||
} else {
|
||||
// Load Chats
|
||||
Task {
|
||||
switch fetchChats() {
|
||||
case let .success(chats):
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = UIImage(base64Encoded: profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
await MainActor.run {
|
||||
self.chats = chats
|
||||
self.profileImages = profileImages
|
||||
withAnimation { isLoaded = true }
|
||||
}
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
}
|
||||
await MainActor.run {
|
||||
self.chats = chats
|
||||
self.profileImages = profileImages
|
||||
withAnimation { isLoaded = true }
|
||||
}
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
}
|
||||
// Process Attachment
|
||||
Task {
|
||||
switch await self.itemProvider!.sharedContent() {
|
||||
case let .success(chatItemContent):
|
||||
await MainActor.run {
|
||||
self.sharedContent = chatItemContent
|
||||
self.bottomBar = .sendButton
|
||||
if case let .text(string) = chatItemContent { comment = string }
|
||||
}
|
||||
case let .failure(errorAlert):
|
||||
await MainActor.run { self.errorAlert = errorAlert }
|
||||
}
|
||||
// Process Attachment
|
||||
Task {
|
||||
switch await getSharedContent(self.itemProvider!) {
|
||||
case let .success(chatItemContent):
|
||||
await MainActor.run {
|
||||
self.sharedContent = chatItemContent
|
||||
self.bottomBar = .sendButton
|
||||
if case let .text(string) = chatItemContent { comment = string }
|
||||
}
|
||||
case let .failure(errorAlert):
|
||||
await MainActor.run { self.errorAlert = errorAlert }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,14 +168,15 @@ class ShareModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func initChat() -> ErrorAlert? {
|
||||
private func initChat(with dbKey: String? = nil) -> ErrorAlert? {
|
||||
do {
|
||||
if hasChatCtrl() {
|
||||
if hasChatCtrl() && dbKey == nil {
|
||||
try apiActivateChat()
|
||||
} else {
|
||||
resetChatCtrl() // Clears retained migration result
|
||||
registerGroupDefaults()
|
||||
haskell_init_se()
|
||||
let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
|
||||
let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation())
|
||||
if let e = migrationError(result) { return e }
|
||||
try apiSetAppFilePaths(
|
||||
filesFolder: getAppFilesDirectory().path,
|
||||
@@ -171,8 +195,9 @@ class ShareModel: ObservableObject {
|
||||
private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? {
|
||||
let useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
let storedDBKey = kcDatabasePassword.get()
|
||||
// This switch duplicates DatabaseErrorView.
|
||||
// TODO allow entering passphrase and make messages the same as in DatabaseErrorView.
|
||||
if case .errorNotADatabase = r {
|
||||
Task { await MainActor.run { self.alertRequiresPassword = true } }
|
||||
}
|
||||
return switch r {
|
||||
case .errorNotADatabase:
|
||||
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
||||
@@ -182,8 +207,8 @@ class ShareModel: ObservableObject {
|
||||
)
|
||||
} else {
|
||||
ErrorAlert(
|
||||
title: "Encrypted database",
|
||||
message: "Sharing is not supported when passphrase is not stored in KeyChain."
|
||||
title: "Database encrypted!",
|
||||
message: "Database passphrase is required to open chat."
|
||||
)
|
||||
}
|
||||
case let .errorMigration(_, migrationError):
|
||||
@@ -363,100 +388,104 @@ enum SharedContent {
|
||||
switch self {
|
||||
case let .image(preview, _): .image(text: comment, image: preview)
|
||||
case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration)
|
||||
case let .url(preview): .link(text: comment, preview: preview)
|
||||
case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview)
|
||||
case .text: .text(comment)
|
||||
case .data: .file(comment)
|
||||
}
|
||||
}
|
||||
|
||||
func prohibited(in chatData: ChatData, hasSimplexLink: Bool) -> Bool {
|
||||
chatData.prohibitedByPref(
|
||||
hasSimplexLink: hasSimplexLink,
|
||||
isMediaOrFileAttachment: cryptoFile != nil,
|
||||
isVoice: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSItemProvider {
|
||||
fileprivate func sharedContent() async -> Result<SharedContent, ErrorAlert> {
|
||||
if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) {
|
||||
switch type {
|
||||
// Prepare Image message
|
||||
case .image:
|
||||
|
||||
// Animated
|
||||
return if hasItemConformingToTypeIdentifier(UTType.gif.identifier) {
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let image = UIImage(data: data),
|
||||
let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Static
|
||||
} else {
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE),
|
||||
let cryptoFile = saveImage(image),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
}
|
||||
|
||||
// Prepare Movie message
|
||||
case .movie:
|
||||
fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result<SharedContent, ErrorAlert> {
|
||||
if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) {
|
||||
switch type {
|
||||
// Prepare Image message
|
||||
case .image:
|
||||
// Animated
|
||||
return if ip.hasItemConformingToTypeIdentifier(UTType.gif.identifier) {
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let trancodedUrl = await transcodeVideo(from: url),
|
||||
let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE),
|
||||
let cryptoFile = moveTempFileFromURL(trancodedUrl) {
|
||||
try? FileManager.default.removeItem(at: trancodedUrl)
|
||||
return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile))
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Data message
|
||||
case .fileURL:
|
||||
if let url = try? await inPlaceUrl(type: .data) {
|
||||
if isFileTooLarge(for: url) {
|
||||
let sizeString = ByteCountFormatter.string(
|
||||
fromByteCount: Int64(getMaxFileSize(.xftp)),
|
||||
countStyle: .binary
|
||||
)
|
||||
return .failure(
|
||||
ErrorAlert(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(sizeString)."
|
||||
)
|
||||
)
|
||||
}
|
||||
if let file = saveFileFromURL(url) {
|
||||
return .success(.data(cryptoFile: file))
|
||||
}
|
||||
}
|
||||
return .failure(ErrorAlert("Error preparing file"))
|
||||
|
||||
// Prepare Link message
|
||||
case .url:
|
||||
if let url = try? await loadItem(forTypeIdentifier: type.identifier) as? URL {
|
||||
let content: SharedContent =
|
||||
// Option to disable previews needs to be taken into account
|
||||
// if let linkPreview = await getLinkPreview(for: url) {
|
||||
// .url(preview: linkPreview)
|
||||
// } else {
|
||||
.text(string: url.absoluteString)
|
||||
// }
|
||||
return .success(content)
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Text message
|
||||
case .text:
|
||||
return if let text = try? await loadItem(forTypeIdentifier: type.identifier) as? String {
|
||||
.success(.text(string: text))
|
||||
let data = try? Data(contentsOf: url),
|
||||
let image = UIImage(data: data),
|
||||
let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Static
|
||||
} else {
|
||||
if let image = await staticImage(),
|
||||
let cryptoFile = saveImage(image),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) {
|
||||
.success(.image(preview: preview, cryptoFile: cryptoFile))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
default: return .failure(ErrorAlert("Unsupported format"))
|
||||
}
|
||||
} else {
|
||||
return .failure(ErrorAlert("Unsupported format"))
|
||||
|
||||
// Prepare Movie message
|
||||
case .movie:
|
||||
if let url = try? await inPlaceUrl(type: type),
|
||||
let trancodedUrl = await transcodeVideo(from: url),
|
||||
let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(),
|
||||
let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE),
|
||||
let cryptoFile = moveTempFileFromURL(trancodedUrl) {
|
||||
try? FileManager.default.removeItem(at: trancodedUrl)
|
||||
return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile))
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Data message
|
||||
case .fileURL:
|
||||
if let url = try? await inPlaceUrl(type: .data) {
|
||||
if isFileTooLarge(for: url) {
|
||||
let sizeString = ByteCountFormatter.string(
|
||||
fromByteCount: Int64(getMaxFileSize(.xftp)),
|
||||
countStyle: .binary
|
||||
)
|
||||
return .failure(
|
||||
ErrorAlert(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(sizeString)."
|
||||
)
|
||||
)
|
||||
}
|
||||
if let file = saveFileFromURL(url) {
|
||||
return .success(.data(cryptoFile: file))
|
||||
}
|
||||
}
|
||||
return .failure(ErrorAlert("Error preparing file"))
|
||||
|
||||
// Prepare Link message
|
||||
case .url:
|
||||
if let url = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? URL {
|
||||
let content: SharedContent =
|
||||
if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) {
|
||||
.url(preview: linkPreview)
|
||||
} else {
|
||||
.text(string: url.absoluteString)
|
||||
}
|
||||
return .success(content)
|
||||
} else { return .failure(ErrorAlert("Error preparing message")) }
|
||||
|
||||
// Prepare Text message
|
||||
case .text:
|
||||
return if let text = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? String {
|
||||
.success(.text(string: text))
|
||||
} else { .failure(ErrorAlert("Error preparing message")) }
|
||||
default: return .failure(ErrorAlert("Unsupported format"))
|
||||
}
|
||||
} else {
|
||||
return .failure(ErrorAlert("Unsupported format"))
|
||||
}
|
||||
|
||||
private func inPlaceUrl(type: UTType) async throws -> URL {
|
||||
|
||||
func inPlaceUrl(type: UTType) async throws -> URL {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
let _ = loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in
|
||||
let _ = ip.loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in
|
||||
if let url = url {
|
||||
cont.resume(returning: url)
|
||||
} else if let error = error {
|
||||
@@ -468,12 +497,23 @@ extension NSItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private func firstMatching(of types: Array<UTType>) -> UTType? {
|
||||
func firstMatching(of types: Array<UTType>) -> UTType? {
|
||||
for type in types {
|
||||
if hasItemConformingToTypeIdentifier(type.identifier) { return type }
|
||||
if ip.hasItemConformingToTypeIdentifier(type.identifier) { return type }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func staticImage() async -> UIImage? {
|
||||
if let url = try? await inPlaceUrl(type: .image),
|
||||
let downsampledImage = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE) {
|
||||
downsampledImage
|
||||
} else {
|
||||
/// Fallback to loading image directly from `ItemProvider`
|
||||
/// in case loading from disk is not possible. Required for sharing screenshots.
|
||||
try? await ip.loadItem(forTypeIdentifier: UTType.image.identifier) as? UIImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,24 +12,39 @@ import SimpleXChat
|
||||
struct ShareView: View {
|
||||
@ObservedObject var model: ShareModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var password = String()
|
||||
@AppStorage(GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS, store: groupDefaults) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottom) {
|
||||
if model.isLoaded {
|
||||
List(model.filteredChats) { chat in
|
||||
let isProhibited = model.isProhibited(chat)
|
||||
let isSelected = model.selected == chat
|
||||
HStack {
|
||||
profileImage(
|
||||
chatInfoId: chat.chatInfo.id,
|
||||
systemFallback: chatIconName(chat.chatInfo),
|
||||
iconName: chatIconName(chat.chatInfo),
|
||||
size: 30
|
||||
)
|
||||
Text(chat.chatInfo.displayName)
|
||||
Text(chat.chatInfo.displayName).foregroundStyle(
|
||||
isProhibited ? .secondary : .primary
|
||||
)
|
||||
Spacer()
|
||||
radioButton(selected: chat == model.selected)
|
||||
radioButton(selected: isSelected && !isProhibited)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { model.selected = model.selected == chat ? nil : chat }
|
||||
.onTapGesture {
|
||||
if isProhibited {
|
||||
model.errorAlert = ErrorAlert(
|
||||
title: "Cannot forward message",
|
||||
message: "Selected chat preferences prohibit this message."
|
||||
) { Button("Ok", role: .cancel) { } }
|
||||
} else {
|
||||
model.selected = isSelected ? nil : chat
|
||||
}
|
||||
}
|
||||
.tag(chat)
|
||||
}
|
||||
} else {
|
||||
@@ -53,7 +68,19 @@ struct ShareView: View {
|
||||
placement: .navigationBarDrawer(displayMode: .always)
|
||||
)
|
||||
.alert($model.errorAlert) { alert in
|
||||
Button("Ok") { model.completion() }
|
||||
if model.alertRequiresPassword {
|
||||
SecureField("Passphrase", text: $password)
|
||||
Button("Ok") {
|
||||
model.setup(with: password)
|
||||
password = String()
|
||||
}
|
||||
Button("Cancel", role: .cancel) { model.completion() }
|
||||
} else {
|
||||
Button("Ok") { model.completion() }
|
||||
}
|
||||
}
|
||||
.onChange(of: model.comment) {
|
||||
model.hasSimplexLink = hasSimplexLink($0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +93,7 @@ struct ShareView: View {
|
||||
HStack {
|
||||
Group {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
TextField("Comment", text: $model.comment, axis: .vertical)
|
||||
TextField("Comment", text: $model.comment, axis: .vertical).lineLimit(6)
|
||||
} else {
|
||||
TextField("Comment", text: $model.comment)
|
||||
}
|
||||
@@ -159,17 +186,17 @@ struct ShareView: View {
|
||||
.background(Material.ultraThin)
|
||||
}
|
||||
|
||||
private func profileImage(chatInfoId: ChatInfo.ID, systemFallback: String, size: Double) -> some View {
|
||||
Group {
|
||||
if let uiImage = model.profileImages[chatInfoId] {
|
||||
Image(uiImage: uiImage).resizable()
|
||||
} else {
|
||||
Image(systemName: systemFallback).resizable()
|
||||
}
|
||||
@ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, iconName: String, size: Double) -> some View {
|
||||
if let uiImage = model.profileImages[chatInfoId] {
|
||||
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius)
|
||||
} else {
|
||||
Image(systemName: iconName)
|
||||
.resizable()
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.frame(width: size, height: size)
|
||||
// add background when adding themes to SE
|
||||
// .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear))
|
||||
}
|
||||
.foregroundStyle(Color(.tertiaryLabel))
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: size * 0.225, style: .continuous))
|
||||
}
|
||||
|
||||
private func radioButton(selected: Bool) -> some View {
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; };
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
||||
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; };
|
||||
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; };
|
||||
@@ -529,6 +530,7 @@
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; };
|
||||
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
||||
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||
@@ -716,6 +718,7 @@
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */,
|
||||
);
|
||||
path = Chat;
|
||||
sourceTree = "<group>";
|
||||
@@ -1426,6 +1429,7 @@
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */,
|
||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */,
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */,
|
||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */,
|
||||
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */,
|
||||
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */,
|
||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */,
|
||||
|
||||
@@ -2115,7 +2115,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
|
||||
public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil
|
||||
public var iosCallKitEnabled: Bool? = nil
|
||||
public var iosCallKitCallsInRecents: Bool? = nil
|
||||
public var uiProfileImageCornerRadius: Float? = nil
|
||||
public var uiProfileImageCornerRadius: Double? = nil
|
||||
public var uiColorScheme: String? = nil
|
||||
public var uiDarkColorScheme: String? = nil
|
||||
public var uiCurrentThemeIds: [String: String]? = nil
|
||||
|
||||
@@ -11,6 +11,8 @@ import SwiftUI
|
||||
|
||||
public let appSuspendTimeout: Int = 15 // seconds
|
||||
|
||||
public let defaultProfileImageCorner: Double = 22.5
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||
let GROUP_DEFAULT_SE_STATE = "seState"
|
||||
@@ -20,11 +22,18 @@ public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun"
|
||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||
public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used
|
||||
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used
|
||||
// replaces DEFAULT_PERFORM_LA
|
||||
let GROUP_DEFAULT_PERFORM_LA = "performLocalAuthentication"
|
||||
public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension"
|
||||
// replaces DEFAULT_PRIVACY_LINK_PREVIEWS
|
||||
let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||
public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays"
|
||||
// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS
|
||||
public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
@@ -72,10 +81,14 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_INCOGNITO: false,
|
||||
GROUP_DEFAULT_STORE_DB_PASSPHRASE: true,
|
||||
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
|
||||
GROUP_DEFAULT_PERFORM_LA: true,
|
||||
GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false,
|
||||
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: false,
|
||||
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||
GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true,
|
||||
GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
||||
@@ -190,6 +203,12 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>(
|
||||
|
||||
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
|
||||
|
||||
public let performLAGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PERFORM_LA)
|
||||
|
||||
public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_ALLOW_SHARE_EXTENSION)
|
||||
|
||||
public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
|
||||
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
@@ -197,6 +216,8 @@ public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDef
|
||||
|
||||
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
|
||||
|
||||
public let profileImageCornerRadiusGroupDefault = Default<Double>(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
|
||||
@@ -2454,13 +2454,16 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? {
|
||||
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? {
|
||||
switch (chatInfo, chatDir) {
|
||||
case let (.group(groupInfo), .groupRcv(groupMember)):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
|
||||
? (groupInfo, groupMember)
|
||||
: nil
|
||||
case let (.group(groupInfo), .groupSnd):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .admin ? (groupInfo, nil) : nil
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -2475,6 +2478,10 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var canBeDeletedForSelf: Bool {
|
||||
(content.msgContent != nil && !meta.isLive) || meta.itemDeleted != nil || isDeletedContent || mergeCategory != nil || showLocalDelete
|
||||
}
|
||||
|
||||
public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: dir,
|
||||
|
||||
@@ -14,6 +14,45 @@ public protocol ChatLike {
|
||||
var chatStats: ChatStats { get }
|
||||
}
|
||||
|
||||
extension ChatLike {
|
||||
public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
|
||||
if case let .group(groupInfo) = self.chatInfo {
|
||||
let p = groupInfo.fullGroupPreferences
|
||||
return switch feature {
|
||||
case .timedMessages: p.timedMessages.on
|
||||
case .directMessages: p.directMessages.on(for: groupInfo.membership)
|
||||
case .fullDelete: p.fullDelete.on
|
||||
case .reactions: p.reactions.on
|
||||
case .voice: p.voice.on(for: groupInfo.membership)
|
||||
case .files: p.files.on(for: groupInfo.membership)
|
||||
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
|
||||
case .history: p.history.on
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func prohibitedByPref(
|
||||
hasSimplexLink: Bool,
|
||||
isMediaOrFileAttachment: Bool,
|
||||
isVoice: Bool
|
||||
) -> Bool {
|
||||
// preference checks should match checks in compose view
|
||||
let simplexLinkProhibited = hasSimplexLink && !groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = isMediaOrFileAttachment && !groupFeatureEnabled(.files)
|
||||
let voiceProhibited = isVoice && !chatInfo.featureEnabled(.voice)
|
||||
return switch chatInfo {
|
||||
case .direct: voiceProhibited
|
||||
case .group: simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||
case .local: false
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
|
||||
var filteredChats = chats.filter { c in
|
||||
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
|
||||
@@ -60,3 +99,15 @@ public func chatIconName(_ cInfo: ChatInfo) -> String {
|
||||
default: "circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
public func hasSimplexLink(_ text: String?) -> Bool {
|
||||
if let text, let parsedMsg = parseSimpleXMarkdown(text) {
|
||||
parsedMsgHasSimplexLink(parsedMsg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
public func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
|
||||
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
}
|
||||
|
||||
@@ -72,25 +72,25 @@ extension View {
|
||||
_ errorAlert: Binding<ErrorAlert?>,
|
||||
@ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() }
|
||||
) -> some View {
|
||||
if let alert = errorAlert.wrappedValue {
|
||||
self.alert(
|
||||
alert.title,
|
||||
isPresented: Binding<Bool>(
|
||||
get: { errorAlert.wrappedValue != nil },
|
||||
set: { if !$0 { errorAlert.wrappedValue = nil } }
|
||||
),
|
||||
actions: {
|
||||
if let actions_ = alert.actions {
|
||||
actions_()
|
||||
} else {
|
||||
actions(alert)
|
||||
}
|
||||
},
|
||||
message: {
|
||||
if let message = alert.message { Text(message) }
|
||||
alert(
|
||||
errorAlert.wrappedValue?.title ?? "",
|
||||
isPresented: Binding<Bool>(
|
||||
get: { errorAlert.wrappedValue != nil },
|
||||
set: { if !$0 { errorAlert.wrappedValue = nil } }
|
||||
),
|
||||
actions: {
|
||||
if let actions_ = errorAlert.wrappedValue?.actions {
|
||||
actions_()
|
||||
} else {
|
||||
if let alert = errorAlert.wrappedValue { actions(alert) }
|
||||
}
|
||||
)
|
||||
} else { self }
|
||||
},
|
||||
message: {
|
||||
if let message = errorAlert.wrappedValue?.message {
|
||||
Text(message)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -428,3 +428,22 @@ public func getLinkPreview(for url: URL) async -> LinkPreview? {
|
||||
getLinkPreview(url: url) { cont.resume(returning: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
private let squareToCircleRatio = 0.935
|
||||
|
||||
private let radiusFactor = (1 - squareToCircleRatio) / 50
|
||||
|
||||
@ViewBuilder public func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View {
|
||||
let v = img.resizable()
|
||||
if radius >= 50 {
|
||||
v.frame(width: size, height: size).clipShape(Circle())
|
||||
} else if radius <= 0 {
|
||||
let sz = size * squareToCircleRatio
|
||||
v.frame(width: sz, height: sz).padding((size - sz) / 2)
|
||||
} else {
|
||||
let sz = size * (squareToCircleRatio + radius * radiusFactor)
|
||||
v.frame(width: sz, height: sz)
|
||||
.clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous))
|
||||
.padding((size - sz) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.common.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.DefaultTheme
|
||||
@@ -27,7 +27,6 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -86,7 +85,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.chatRunning.value == true) {
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
kotlin.runCatching {
|
||||
val currentUserId = chatModel.currentUser.value?.userId
|
||||
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId()))
|
||||
@@ -99,7 +98,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
|
||||
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
|
||||
}
|
||||
chatModel.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
|
||||
}
|
||||
|
||||
+7
@@ -6,6 +6,7 @@ import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.onRightClick
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI != null && oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
if (!disabled) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
|
||||
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val onClick = { platform.androidStartCallActivity(false) }
|
||||
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
|
||||
val source = remember { MutableInteractionSource() }
|
||||
|
||||
+3
-1
@@ -78,7 +78,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
@@ -104,6 +104,8 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
@@ -278,7 +278,7 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
}
|
||||
}
|
||||
if (call != null && showCallArea) {
|
||||
ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) })
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+315
-286
@@ -54,7 +54,9 @@ object ChatModel {
|
||||
val ctrlInitInProgress = mutableStateOf(false)
|
||||
val dbMigrationInProgress = mutableStateOf(false)
|
||||
val incompleteInitializedDbRemoved = mutableStateOf(false)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
val chats: State<List<Chat>> = _chats
|
||||
private val chatsContext = ChatsContext()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
val switchingUsersAndHosts = mutableStateOf(false)
|
||||
@@ -126,7 +128,7 @@ object ChatModel {
|
||||
val updatingProgress = mutableStateOf(null as Float?)
|
||||
var updatingRequest: Closeable? = null
|
||||
|
||||
val updatingChatsMutex: Mutex = Mutex()
|
||||
private val updatingChatsMutex: Mutex = Mutex()
|
||||
val changingActiveUserMutex: Mutex = Mutex()
|
||||
|
||||
val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
|
||||
@@ -170,11 +172,11 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
// TODO pass rhId?
|
||||
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
|
||||
fun populateGroupMembersIndexes() {
|
||||
groupMembersIndexes.clear()
|
||||
@@ -192,97 +194,102 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock {
|
||||
chatsContext.action()
|
||||
}
|
||||
|
||||
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
|
||||
val currentStats = currentCInfo.contact.activeConn?.connectionStats
|
||||
val newConn = newCInfo.contact.activeConn
|
||||
val newStats = newConn?.connectionStats
|
||||
if (currentStats != null && newConn != null && newStats == null) {
|
||||
newCInfo = newCInfo.copy(
|
||||
contact = newCInfo.contact.copy(
|
||||
activeConn = newConn.copy(
|
||||
connectionStats = currentStats
|
||||
class ChatsContext {
|
||||
val chats = _chats
|
||||
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
|
||||
val currentStats = currentCInfo.contact.activeConn?.connectionStats
|
||||
val newConn = newCInfo.contact.activeConn
|
||||
val newStats = newConn?.connectionStats
|
||||
if (currentStats != null && newConn != null && newStats == null) {
|
||||
newCInfo = newCInfo.copy(
|
||||
contact = newCInfo.contact.copy(
|
||||
activeConn = newConn.copy(
|
||||
connectionStats = currentStats
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
chats.clear()
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
if (cId != null && getChat(cId) == null) {
|
||||
chatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
chats[i] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
}
|
||||
else -> cItem
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
}
|
||||
|
||||
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
chats.clear()
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
if (cId != null && getChat(cId) == null) {
|
||||
chatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
chats[i] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
}
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
}
|
||||
else -> cItem
|
||||
}
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
@@ -290,123 +297,197 @@ object ChatModel {
|
||||
}
|
||||
else
|
||||
chat.chatStats
|
||||
)
|
||||
if (i > 0) {
|
||||
popChat_(i)
|
||||
)
|
||||
if (i > 0) {
|
||||
chats.add(index = 0, chats.removeAt(i))
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
withContext(Dispatchers.Main) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
return withContext(Dispatchers.Main) {
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
false
|
||||
} else {
|
||||
val status = chatItemStatuses.remove(cItem.id)
|
||||
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
chatItems.add(ci)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
return withContext(Dispatchers.Main) {
|
||||
// update current chat
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
}
|
||||
} else if (status != null) {
|
||||
chatItemStatuses[cItem.id] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
}
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
chatItems.removeAll {
|
||||
val remove = it.id == cItem.id
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val cInfo = chat.chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chat, range)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
|
||||
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembersIndexes[member.groupMemberId]
|
||||
if (memberIndex != null) {
|
||||
groupMembers[memberIndex] = member
|
||||
false
|
||||
} else {
|
||||
val status = chatItemStatuses.remove(cItem.id)
|
||||
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
chatItems.add(ci)
|
||||
groupMembers.add(member)
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1
|
||||
true
|
||||
}
|
||||
} else {
|
||||
res
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
}
|
||||
} else if (status != null) {
|
||||
chatItemStatuses[cItem.id] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
}
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.removeAll {
|
||||
val remove = it.id == cItem.id
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.clear()
|
||||
}
|
||||
}
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
@@ -447,28 +528,6 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val cInfo = chat.chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chat, range)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int {
|
||||
val cInfo = chat.chatInfo
|
||||
var markedRead = 0
|
||||
@@ -493,20 +552,6 @@ object ChatModel {
|
||||
return markedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounter(rhId, user, 1)
|
||||
}
|
||||
@@ -600,11 +645,6 @@ object ChatModel {
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun popChat_(i: Int) {
|
||||
val chat = chats.removeAt(i)
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
|
||||
fun replaceConnReqView(id: String, withId: String) {
|
||||
if (id == showingInvitation.value?.connId) {
|
||||
showingInvitation.value = null
|
||||
@@ -629,41 +669,6 @@ object ChatModel {
|
||||
showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
|
||||
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembersIndexes[member.groupMemberId]
|
||||
if (memberIndex != null) {
|
||||
groupMembers[memberIndex] = member
|
||||
false
|
||||
} else {
|
||||
groupMembers.add(member)
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
|
||||
val conn = contact.activeConn
|
||||
if (conn != null) {
|
||||
@@ -802,6 +807,7 @@ interface SomeChat {
|
||||
val id: ChatId
|
||||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val chatDeleted: Boolean
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
@@ -882,6 +888,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contact.id
|
||||
override val apiId get() = contact.apiId
|
||||
override val ready get() = contact.ready
|
||||
override val chatDeleted get() = contact.chatDeleted
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
@@ -906,6 +913,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = groupInfo.id
|
||||
override val apiId get() = groupInfo.apiId
|
||||
override val ready get() = groupInfo.ready
|
||||
override val chatDeleted get() = groupInfo.chatDeleted
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
@@ -930,6 +938,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = noteFolder.id
|
||||
override val apiId get() = noteFolder.apiId
|
||||
override val ready get() = noteFolder.ready
|
||||
override val chatDeleted get() = noteFolder.chatDeleted
|
||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||
override val incognito get() = noteFolder.incognito
|
||||
@@ -954,6 +963,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactRequest.id
|
||||
override val apiId get() = contactRequest.apiId
|
||||
override val ready get() = contactRequest.ready
|
||||
override val chatDeleted get() = contactRequest.chatDeleted
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
@@ -978,6 +988,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactConnection.id
|
||||
override val apiId get() = contactConnection.apiId
|
||||
override val ready get() = contactConnection.ready
|
||||
override val chatDeleted get() = contactConnection.chatDeleted
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = contactConnection.incognito
|
||||
@@ -1003,6 +1014,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
@@ -1079,6 +1091,7 @@ data class Contact(
|
||||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean,
|
||||
override val chatDeleted: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
@@ -1153,6 +1166,7 @@ data class Contact(
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
contactGrpInvSent = false,
|
||||
chatDeleted = false,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
@@ -1161,7 +1175,8 @@ data class Contact(
|
||||
@Serializable
|
||||
enum class ContactStatus {
|
||||
@SerialName("active") Active,
|
||||
@SerialName("deleted") Deleted;
|
||||
@SerialName("deleted") Deleted,
|
||||
@SerialName("deletedByUser") DeletedByUser;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1304,6 +1319,7 @@ data class GroupInfo (
|
||||
override val id get() = "#$groupId"
|
||||
override val apiId get() = groupId
|
||||
override val ready get() = membership.memberActive
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||
override val incognito get() = membership.memberIncognito
|
||||
@@ -1609,6 +1625,7 @@ class NoteFolder(
|
||||
override val chatType get() = ChatType.Local
|
||||
override val id get() = "*$noteFolderId"
|
||||
override val apiId get() = noteFolderId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1645,6 +1662,7 @@ class UserContactRequest (
|
||||
override val chatType get() = ChatType.ContactRequest
|
||||
override val id get() = "<@$contactRequestId"
|
||||
override val apiId get() = contactRequestId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1684,6 +1702,7 @@ class PendingContactConnection(
|
||||
override val chatType get() = ChatType.ContactConnection
|
||||
override val id get () = ":$pccConnId"
|
||||
override val apiId get() = pccConnId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -2108,45 +2127,55 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.add(index: Int, elem: T) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); add(index, elem) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.add(elem: T) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); add(elem) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); addAll(index, elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); addAll(elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeAll(block: (T) -> Boolean) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); removeAll(block) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeAt(index: Int): T {
|
||||
val new = SnapshotStateList<T>()
|
||||
new.addAll(value)
|
||||
val res = new.removeAt(index)
|
||||
value = new
|
||||
return res
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeLast() {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); removeLast() }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
|
||||
value = SnapshotStateList<ChatItem>()
|
||||
fun <T> MutableState<SnapshotStateList<T>>.clear() {
|
||||
value = SnapshotStateList<T>()
|
||||
}
|
||||
|
||||
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
|
||||
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
|
||||
|
||||
val State<List<ChatItem>>.size: Int get() = value.size
|
||||
fun <T> State<SnapshotStateList<T>>.toList(): List<T> = value.toList()
|
||||
|
||||
operator fun <T> State<SnapshotStateList<T>>.get(i: Int): T = value[i]
|
||||
|
||||
operator fun <T> State<SnapshotStateList<T>>.set(index: Int, elem: T) { value[index] = elem }
|
||||
|
||||
val State<List<Any>>.size: Int get() = value.size
|
||||
|
||||
enum class CIMergeCategory {
|
||||
MemberConnected,
|
||||
|
||||
+212
-87
@@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -217,13 +217,16 @@ class AppPreferences {
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true)
|
||||
val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true)
|
||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, false)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
get = fun() = settings.getInt(prefName, default),
|
||||
@@ -381,6 +384,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||
@@ -401,6 +405,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
@@ -478,9 +484,9 @@ object ChatController {
|
||||
}
|
||||
Log.d(TAG, "startChat: started")
|
||||
} else {
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
val chats = apiGetChats(null)
|
||||
chatModel.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
Log.d(TAG, "startChat: running")
|
||||
}
|
||||
@@ -558,9 +564,9 @@ object ChatController {
|
||||
val hasUser = chatModel.currentUser.value != null
|
||||
chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null
|
||||
chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
val chats = apiGetChats(rhId)
|
||||
chatModel.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,16 +1183,18 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
||||
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val cInfo = chat.chatInfo
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
|
||||
withChats {
|
||||
removeChat(chat.remoteHostId, cInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean {
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val success = when {
|
||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||
@@ -1207,11 +1215,29 @@ object ChatController {
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? {
|
||||
val type = ChatType.Direct
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val contact = when {
|
||||
r is CR.ContactDeleted -> r.contact
|
||||
else -> {
|
||||
val titleId = MR.strings.error_deleting_contact
|
||||
apiErrorAlert("apiDeleteChat", generalGetString(titleId), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
chatModel.deletedChats.value -= rh to type.type + id
|
||||
return contact
|
||||
}
|
||||
|
||||
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||
if (updatedChatInfo != null) {
|
||||
chatModel.clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
withChats {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -1546,10 +1572,12 @@ object ChatController {
|
||||
val r = sendCmd(rh, CC.ApiJoinGroup(groupId))
|
||||
when (r) {
|
||||
is CR.UserAcceptedGroupSent ->
|
||||
chatModel.updateGroup(rh, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rh, r.groupInfo)
|
||||
}
|
||||
is CR.ChatCmdError -> {
|
||||
val e = r.chatError
|
||||
suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } }
|
||||
suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } }
|
||||
if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) {
|
||||
deleteGroup()
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired))
|
||||
@@ -1703,7 +1731,9 @@ object ChatController {
|
||||
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
|
||||
val toContact = apiSetContactPrefs(rh, contact.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
chatModel.updateContact(rh, toContact)
|
||||
withChats {
|
||||
updateContact(rh, toContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1973,16 +2003,20 @@ object ChatController {
|
||||
when (r) {
|
||||
is CR.ContactDeletedByContact -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactConnected -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (r.contact.directOrUsed) {
|
||||
@@ -1992,21 +2026,25 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactConnecting -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactSndReady -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected())
|
||||
@@ -2015,10 +2053,12 @@ object ChatController {
|
||||
val contactRequest = r.contactRequest
|
||||
val cInfo = ChatInfo.ContactRequest(contactRequest)
|
||||
if (active(r.user)) {
|
||||
if (chatModel.hasChat(rhId, contactRequest.id)) {
|
||||
chatModel.updateChatInfo(rhId, cInfo)
|
||||
} else {
|
||||
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
|
||||
withChats {
|
||||
if (chatModel.hasChat(rhId, contactRequest.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
|
||||
}
|
||||
}
|
||||
}
|
||||
ntfManager.notifyContactRequestReceived(r.user, cInfo)
|
||||
@@ -2026,12 +2066,16 @@ object ChatController {
|
||||
is CR.ContactUpdated -> {
|
||||
if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) {
|
||||
val cInfo = ChatInfo.Direct(r.toContact)
|
||||
chatModel.updateChatInfo(rhId, cInfo)
|
||||
withChats {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberUpdated -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
@@ -2039,7 +2083,9 @@ object ChatController {
|
||||
if (chatModel.chatId.value == r.mergedContact.id) {
|
||||
chatModel.chatId.value = r.intoContact.id
|
||||
}
|
||||
chatModel.removeChat(rhId, r.mergedContact.id)
|
||||
withChats {
|
||||
removeChat(rhId, r.mergedContact.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI,
|
||||
@@ -2049,7 +2095,9 @@ object ChatController {
|
||||
is CR.ContactSubSummary -> {
|
||||
for (sub in r.contactSubscriptions) {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, sub.contact)
|
||||
withChats {
|
||||
updateContact(rhId, sub.contact)
|
||||
}
|
||||
}
|
||||
val err = sub.contactError
|
||||
if (err == null) {
|
||||
@@ -2073,7 +2121,15 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
withChats {
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
}
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
@@ -2094,14 +2150,18 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (!cItem.isDeletedContent && active(r.user)) {
|
||||
chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.ChatItemReaction -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
withChats {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemsDeleted -> {
|
||||
@@ -2127,82 +2187,113 @@ object ChatController {
|
||||
generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description)
|
||||
)
|
||||
}
|
||||
if (toChatItem == null) {
|
||||
chatModel.removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
chatModel.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
withChats {
|
||||
if (toChatItem == null) {
|
||||
removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated
|
||||
withChats {
|
||||
// update so that repeat group invitations are not duplicated
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
// TODO NtfManager.shared.notifyGroupInvitation
|
||||
}
|
||||
}
|
||||
is CR.UserAcceptedGroupSent -> {
|
||||
if (!active(r.user)) return
|
||||
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val conn = r.hostContact?.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
val conn = r.hostContact?.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.GroupLinkConnecting -> {
|
||||
if (!active(r.user)) return
|
||||
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, hostConn.id)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
removeChat(rhId, hostConn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMemberConnecting ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMemberUser -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
}
|
||||
is CR.LeftMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRole ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRoleUser ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberBlockedForAll ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.GroupDeleted -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.UserJoinedGroup ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.ConnectedToGroupMember -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
if (r.memberContact != null) {
|
||||
chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected())
|
||||
@@ -2210,11 +2301,15 @@ object ChatController {
|
||||
}
|
||||
is CR.GroupUpdated ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.toGroup)
|
||||
withChats {
|
||||
updateGroup(rhId, r.toGroup)
|
||||
}
|
||||
}
|
||||
is CR.NewMemberContactReceivedInv ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.RcvFileStart ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
@@ -2314,19 +2409,27 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactSwitch ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberSwitch ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.ContactRatchetSync ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberRatchetSync ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.RemoteHostSessionCode -> {
|
||||
chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode)
|
||||
@@ -2338,7 +2441,9 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactDisabled -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.RemoteHostStopped -> {
|
||||
@@ -2459,7 +2564,9 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactPQEnabled ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.ChatRespError -> when {
|
||||
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
|
||||
@@ -2535,7 +2642,9 @@ object ChatController {
|
||||
suspend fun leaveGroup(rh: Long?, groupId: Long) {
|
||||
val groupInfo = apiLeaveGroup(rh, groupId)
|
||||
if (groupInfo != null) {
|
||||
chatModel.updateGroup(rh, groupInfo)
|
||||
withChats {
|
||||
updateGroup(rh, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2545,7 +2654,7 @@ object ChatController {
|
||||
val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) }
|
||||
if (!activeUser(rh, user)) {
|
||||
notify()
|
||||
} else if (chatModel.upsertChatItem(rh, cInfo, cItem)) {
|
||||
} else if (withChats { upsertChatItem(rh, cInfo, cItem) }) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
@@ -2589,7 +2698,9 @@ object ChatController {
|
||||
chatModel.currentUser.value = user
|
||||
if (user == null) {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chats.clear()
|
||||
withChats {
|
||||
chats.clear()
|
||||
}
|
||||
}
|
||||
val statuses = apiGetNetworkStatuses(rhId)
|
||||
if (statuses != null) {
|
||||
@@ -2790,7 +2901,7 @@ sealed class CC {
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
|
||||
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||
class ApiListContacts(val userId: Long): CC()
|
||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||
@@ -2943,11 +3054,7 @@ sealed class CC {
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
is ApiDeleteChat -> if (notify != null) {
|
||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
||||
} else {
|
||||
"/_delete ${chatRef(type, id)}"
|
||||
}
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
@@ -3169,8 +3276,6 @@ sealed class CC {
|
||||
null
|
||||
}
|
||||
|
||||
private fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||
|
||||
companion object {
|
||||
@@ -3180,6 +3285,8 @@ sealed class CC {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
@@ -5147,6 +5254,19 @@ fun chatError(r: CR): ChatErrorType? {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ChatDeleteMode {
|
||||
@Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("messages") class Messages: ChatDeleteMode()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is ChatDeleteMode.Full -> "full notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Messages -> "messages"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ConnectionPlan {
|
||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||
@@ -6034,6 +6154,7 @@ data class AppSettings(
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
var uiThemes: List<ThemeOverrides>? = null,
|
||||
var oneHandUI: Boolean? = null
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
@@ -6064,6 +6185,7 @@ data class AppSettings(
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||
if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI }
|
||||
return empty
|
||||
}
|
||||
|
||||
@@ -6102,6 +6224,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||
oneHandUI?.let { def.oneHandUI.set(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -6133,6 +6256,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
uiThemes = null,
|
||||
oneHandUI = false
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
@@ -6165,6 +6289,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
uiThemes = def.themeOverrides.get(),
|
||||
oneHandUI = def.oneHandUI.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+462
-45
@@ -12,18 +12,23 @@ import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import androidx.compose.ui.text.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -31,6 +36,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -54,10 +60,11 @@ fun ChatInfoView(
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
close: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val contact = rememberUpdatedState(contact).value
|
||||
val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } }
|
||||
val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } }
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
@@ -102,7 +109,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -114,7 +123,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -124,7 +135,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -135,7 +148,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -151,14 +166,16 @@ fun ChatInfoView(
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.updateContact(
|
||||
chatRh,
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
withChats {
|
||||
updateContact(
|
||||
chatRh,
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
r
|
||||
}
|
||||
},
|
||||
@@ -166,7 +183,9 @@ fun ChatInfoView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
close = close,
|
||||
onSearchClicked = onSearchClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -201,34 +220,42 @@ sealed class SendReceipts {
|
||||
|
||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contact = chatInfo.contact
|
||||
when {
|
||||
contact.sndReady && contact.active && !chatInfo.chatDeleted ->
|
||||
deleteContactOrConversationDialog(chat, contact, chatModel, close)
|
||||
|
||||
contact.sndReady && contact.active && chatInfo.chatDeleted ->
|
||||
deleteContactWithoutConversation(chat, chatModel, close)
|
||||
|
||||
else -> // !(contact.sndReady && contact.active)
|
||||
deleteNotReadyContact(chat, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
||||
buttons = {
|
||||
Column {
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.sndReady && chatInfo.contact.active) {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = true)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = false)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
// Only delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages())
|
||||
if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) {
|
||||
showDeleteConversationNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteActiveContactDialog(chat, contact, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
@@ -241,13 +268,207 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
private fun showDeleteConversationNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.conversation_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteConversationNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
sealed class ContactDeleteMode {
|
||||
class Full: ContactDeleteMode()
|
||||
class Entity: ContactDeleteMode()
|
||||
|
||||
fun toChatDeleteMode(notify: Boolean): ChatDeleteMode =
|
||||
when (this) {
|
||||
is Full -> ChatDeleteMode.Full(notify)
|
||||
is Entity -> ChatDeleteMode.Entity(notify)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val contactDeleteMode = mutableStateOf<ContactDeleteMode>(ContactDeleteMode.Full())
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
Column {
|
||||
// Keep conversation toggle
|
||||
SectionItemView {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(stringResource(MR.strings.keep_conversation))
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
DefaultSwitch(
|
||||
checked = contactDeleteMode.value is ContactDeleteMode.Entity,
|
||||
onCheckedChange = {
|
||||
contactDeleteMode.value =
|
||||
if (it) ContactDeleteMode.Entity() else ContactDeleteMode.Full()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete contact and notify
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = true))
|
||||
if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
Column {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = true)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.delete_and_notify_contact),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.delete_without_notification),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(MR.strings.cancel_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
// Confirm
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.confirm_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(MR.strings.cancel_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDeleteContactNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.contact_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteContactNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||
if (ct != null) {
|
||||
withChats {
|
||||
when (chatDeleteMode) {
|
||||
is ChatDeleteMode.Full ->
|
||||
removeChat(chatRh, chatInfo.id)
|
||||
is ChatDeleteMode.Entity ->
|
||||
updateContact(chatRh, ct)
|
||||
is ChatDeleteMode.Messages ->
|
||||
clearChat(chatRh, ChatInfo.Direct(ct))
|
||||
}
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -300,6 +521,8 @@ fun ChatInfoLayout(
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
close: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -319,7 +542,27 @@ fun ChatInfoLayout(
|
||||
}
|
||||
|
||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SearchButton(chat, contact, close, onSearchClicked)
|
||||
Spacer(Modifier.weight(1f))
|
||||
AudioCallButton(chat, contact)
|
||||
Spacer(Modifier.weight(1f))
|
||||
VideoButton(chat, contact)
|
||||
Spacer(Modifier.weight(1f))
|
||||
MuteButton(chat, contact)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||
SectionItemViewSpaceBetween {
|
||||
@@ -347,7 +590,7 @@ fun ChatInfoLayout(
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
@@ -535,6 +778,174 @@ fun LocalAliasEditor(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(chat: Chat, contact: Contact, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
val disabled = !contact.ready || chat.chatItems.isEmpty()
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_search),
|
||||
title = generalGetString(MR.strings.info_view_search_button),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid) {
|
||||
close.invoke()
|
||||
}
|
||||
onSearchClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MuteButton(chat: Chat, contact: Contact) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
val disabled = !contact.ready || !contact.active
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
toggleNotifications(chat, !ntfsEnabled.value, chatModel, ntfsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AudioCallButton(chat: Chat, contact: Contact) {
|
||||
CallButton(
|
||||
chat,
|
||||
contact,
|
||||
icon = painterResource(MR.images.ic_call),
|
||||
title = generalGetString(MR.strings.info_view_call_button),
|
||||
mediaType = CallMediaType.Audio
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoButton(chat: Chat, contact: Contact) {
|
||||
CallButton(
|
||||
chat,
|
||||
contact,
|
||||
icon = painterResource(MR.images.ic_videocam),
|
||||
title = generalGetString(MR.strings.info_view_video_button),
|
||||
mediaType = CallMediaType.Video
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallButton(chat: Chat, contact: Contact, icon: Painter, title: String, mediaType: CallMediaType) {
|
||||
val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null
|
||||
val needToAllowCallsToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) }
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = icon,
|
||||
title = title,
|
||||
disabled = chatModel.activeCall.value != null,
|
||||
disabledLook = !canCall,
|
||||
onClick =
|
||||
when {
|
||||
canCall -> { { startChatCall(chat, mediaType) } }
|
||||
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
|
||||
!contact.active -> { { showCantCallContactDeletedAlert() } }
|
||||
!contact.ready -> { { showCantCallContactConnectingAlert() } }
|
||||
needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } }
|
||||
!allowedCallsByPrefs -> { { showCallsProhibitedAlert() }}
|
||||
else -> { { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.cant_call_contact_alert_title)) } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactSendMessageAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_member_send_message_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactConnectingAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_contact_connecting_wait_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactDeletedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_contact_deleted_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNeedToAllowCallsAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.allow_calls_question),
|
||||
text = generalGetString(MR.strings.you_need_to_allow_calls),
|
||||
confirmText = generalGetString(MR.strings.allow_verb),
|
||||
dismissText = generalGetString(MR.strings.cancel_verb),
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
private fun allowCallsToContact(chat: Chat) {
|
||||
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
|
||||
withBGApi {
|
||||
chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Calls)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCallsProhibitedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.calls_prohibited_alert_title),
|
||||
text = generalGetString(MR.strings.calls_prohibited_ask_to_enable_calls_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
// for ChatInfoView (it has most buttons - 4) we use Spacer(Modifier.weight(1f)) to fit,
|
||||
// for GroupChat And GroupMemberInfoViews (2 to 3 buttons) we use this as approximately equal to spacing in ChatInfoView
|
||||
val INFO_VIEW_BUTTONS_PADDING = 36.dp
|
||||
|
||||
@Composable
|
||||
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, disabledLook: Boolean, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
enabled = !disabled
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
title.capitalize(Locale.current),
|
||||
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
@@ -768,10 +1179,12 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
|
||||
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||
withChats {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -779,7 +1192,9 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
|
||||
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
||||
chatModel.updateContact(chatRh, it)
|
||||
withChats {
|
||||
updateContact(chatRh, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +1262,8 @@ fun PreviewChatInfoLayout() {
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
close = {},
|
||||
onSearchClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+108
-72
@@ -11,7 +11,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -25,6 +24,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
@@ -45,8 +45,9 @@ import kotlin.math.sign
|
||||
|
||||
@Composable
|
||||
fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) }
|
||||
val activeChat = remember { mutableStateOf(chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatId }) }
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val user = chatModel.currentUser.value
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
@@ -74,6 +75,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (activeChat.value?.id != chatId) {
|
||||
// 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
|
||||
showSearch.value = false
|
||||
activeChat.value = chatModel.getChat(chatId)
|
||||
}
|
||||
markUnreadChatAsRead(activeChat, chatModel)
|
||||
@@ -87,7 +89,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
* TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that
|
||||
* */
|
||||
try {
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
||||
chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
null
|
||||
@@ -112,7 +114,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
// 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.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@@ -190,7 +192,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) {
|
||||
showSearch.value = true
|
||||
}
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
@@ -201,7 +205,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}, close, { showSearch.value = true })
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
@@ -268,10 +272,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (deleted != null) {
|
||||
deletedChatItem = deleted.deletedChatItem.chatItem
|
||||
toChatItem = deleted.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
} else {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
withChats {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,8 +290,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemIds, CIDeleteMode.cidmInternal
|
||||
)
|
||||
if (deleted != null) {
|
||||
for (di in deleted) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
withChats {
|
||||
for (di in deleted) {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,18 +311,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
startCall = out@{ media -> startChatCall(chat, media) },
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -351,7 +348,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -361,7 +360,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +371,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withBGApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -378,7 +381,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -399,7 +404,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
withChats {
|
||||
updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -443,36 +450,20 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) },
|
||||
openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) },
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
withChats {
|
||||
markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
@@ -488,6 +479,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
showSearch = showSearch
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -518,6 +510,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
|
||||
fun startChatCall(chat: Chat, media: CallMediaType) {
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chat.remoteHostId, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatLayout(
|
||||
chat: Chat,
|
||||
@@ -559,10 +564,12 @@ fun ChatLayout(
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
onComposed: suspend (chatId: String) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
showViaProxy: Boolean,
|
||||
showSearch: MutableState<Boolean>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -602,7 +609,7 @@ fun ChatLayout(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) },
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) },
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
@@ -648,16 +655,17 @@ fun ChatInfoToolbar(
|
||||
openGroupLink: (GroupInfo) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
showSearch: MutableState<Boolean>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBackClicked = {
|
||||
if (!showSearch) {
|
||||
if (!showSearch.value) {
|
||||
back()
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch = false
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
@@ -668,9 +676,10 @@ fun ChatInfoToolbar(
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
if (chat.chatInfo is ChatInfo.Local) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
showSearch = true
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chat.chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
@@ -684,7 +693,7 @@ fun ChatInfoToolbar(
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch = true
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -783,6 +792,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
menuItems.add {
|
||||
@@ -810,10 +820,10 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } },
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chat.chatInfo) },
|
||||
onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = barButtons
|
||||
)
|
||||
@@ -1141,7 +1151,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||
.collect {
|
||||
try {
|
||||
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) {
|
||||
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) {
|
||||
if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0)
|
||||
} else {
|
||||
if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance)
|
||||
@@ -1245,7 +1255,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
painterResource(MR.images.ic_check),
|
||||
onClick = {
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
)
|
||||
showDropDown.value = false
|
||||
@@ -1332,6 +1342,28 @@ private fun TopEndFloatingButton(
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
close?.invoke()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId)
|
||||
close?.invoke()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomEndFloatingButton(
|
||||
unreadCount: Int,
|
||||
showButtonWithCounter: Boolean,
|
||||
@@ -1388,8 +1420,10 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
|
||||
false
|
||||
)
|
||||
if (success && chat.id == activeChat.value?.id) {
|
||||
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
|
||||
chatModel.replaceChat(chatRh, chat.id, activeChat.value!!)
|
||||
withChats {
|
||||
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
|
||||
replaceChat(chatRh, chat.id, activeChat.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1587,6 +1621,7 @@ fun PreviewChatLayout() {
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1661,6 +1696,7 @@ fun PreviewGroupChatLayout() {
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
@@ -393,7 +394,9 @@ fun ComposeView(
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
withChats {
|
||||
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
}
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(file.filePath)
|
||||
@@ -421,7 +424,9 @@ fun ComposeView(
|
||||
ttl = ttl
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatModel.addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
withChats {
|
||||
addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
return chatItem
|
||||
}
|
||||
@@ -458,7 +463,9 @@ fun ComposeView(
|
||||
val mc = checkLinkPreview()
|
||||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact)
|
||||
withChats {
|
||||
updateContact(chat.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +481,9 @@ fun ComposeView(
|
||||
mc = updateMsgContent(oldMsgContent),
|
||||
live = live
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
if (updatedItem != null) withChats {
|
||||
upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
}
|
||||
return updatedItem?.chatItem
|
||||
}
|
||||
return null
|
||||
|
||||
+5
-2
@@ -19,6 +19,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -40,8 +41,10 @@ fun ContactPreferencesView(
|
||||
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
|
||||
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
m.updateContact(rhId, toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
withChats {
|
||||
updateContact(rhId, toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+5
-2
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -58,7 +59,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
||||
for (contactId in selectedContacts) {
|
||||
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
||||
if (member != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -81,7 +84,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
val memberContactIds = chatModel.groupMembers
|
||||
.filter { it.memberCurrent }
|
||||
.mapNotNull { it.memberContactId }
|
||||
return chatModel.chats
|
||||
return chatModel.chats.value
|
||||
.asSequence()
|
||||
.map { it.chatInfo }
|
||||
.filterIsInstance<ChatInfo.Direct>()
|
||||
|
||||
+89
-12
@@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -40,10 +41,10 @@ import kotlinx.coroutines.launch
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
|
||||
@@ -113,7 +114,8 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
|
||||
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
}
|
||||
},
|
||||
onSearchClicked = onSearchClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -131,13 +133,15 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chat.remoteHostId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
withChats {
|
||||
removeChat(chat.remoteHostId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,7 +173,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||
withBGApi {
|
||||
val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId)
|
||||
if (updatedMember != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,6 +183,57 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(chat: Chat, group: GroupInfo, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
val disabled = !group.ready || chat.chatItems.isEmpty()
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_search),
|
||||
title = generalGetString(MR.strings.info_view_search_button),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid) {
|
||||
close.invoke()
|
||||
}
|
||||
onSearchClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MuteButton(chat: Chat, groupInfo: GroupInfo) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
disabled = !groupInfo.ready,
|
||||
disabledLook = !groupInfo.ready,
|
||||
onClick = {
|
||||
toggleNotifications(chat, !ntfsEnabled.value, chatModel, ntfsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersButton(chat: Chat, groupInfo: GroupInfo) {
|
||||
InfoViewActionButton(
|
||||
icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500),
|
||||
title = stringResource(MR.strings.action_button_add_members),
|
||||
disabled = !groupInfo.ready,
|
||||
disabledLook = !groupInfo.ready,
|
||||
onClick = {
|
||||
if (groupInfo.incognito) {
|
||||
openGroupLink(groupInfo = groupInfo, rhId = chat.remoteHostId)
|
||||
} else {
|
||||
addGroupMembers(groupInfo = groupInfo, rhId = chat.remoteHostId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
@@ -196,6 +253,8 @@ fun GroupChatInfoLayout(
|
||||
clearChat: () -> Unit,
|
||||
leaveGroup: () -> Unit,
|
||||
manageGroupLink: () -> Unit,
|
||||
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -219,6 +278,24 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SearchButton(chat, groupInfo, close, onSearchClicked)
|
||||
if (groupInfo.canAddMembers) {
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
AddGroupMembersButton(chat, groupInfo)
|
||||
}
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
MuteButton(chat, groupInfo)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
@@ -235,7 +312,7 @@ fun GroupChatInfoLayout(
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
@@ -584,7 +661,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+114
-41
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -52,7 +53,7 @@ fun GroupMemberInfoView(
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
@@ -72,13 +73,15 @@ fun GroupMemberInfoView(
|
||||
withBGApi {
|
||||
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
chatModel.addChat(c)
|
||||
withChats {
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
addChat(c)
|
||||
}
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatItems.replaceAll(c.chatItems)
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatItems.replaceAll(c.chatItems)
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -88,8 +91,10 @@ fun GroupMemberInfoView(
|
||||
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (memberContact != null) {
|
||||
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
chatModel.addChat(memberChat)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
withChats {
|
||||
addChat(memberChat)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
}
|
||||
closeAll()
|
||||
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
|
||||
}
|
||||
@@ -114,7 +119,9 @@ fun GroupMemberInfoView(
|
||||
withBGApi {
|
||||
kotlin.runCatching {
|
||||
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, mem)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, mem)
|
||||
}
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
@@ -127,7 +134,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -139,7 +148,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -150,7 +161,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -161,7 +174,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -177,15 +192,17 @@ fun GroupMemberInfoView(
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
withChats {
|
||||
upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
r
|
||||
}
|
||||
},
|
||||
@@ -211,7 +228,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
|
||||
withBGApi {
|
||||
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
|
||||
if (removedMember != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -246,10 +265,10 @@ fun GroupMemberInfoLayout(
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
fun knownDirectChat(contactId: Long): Chat? {
|
||||
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||
val chat = getContactChat(contactId)
|
||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
chat
|
||||
chat to chat.chatInfo.contact
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -309,17 +328,53 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
AudioCallButton(chat, contact)
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
VideoButton(chat, contact)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
|
||||
} else {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
} else { // no known contact chat && directMessages are off
|
||||
InfoViewActionButton(painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title))
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
|
||||
})
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
@@ -420,6 +475,20 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSendMessageToEnableCallsAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_member_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_member_send_message_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDirectMessagesProhibitedAlert(title: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = generalGetString(MR.strings.direct_messages_are_prohibited_in_chat)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
Column(
|
||||
@@ -513,12 +582,12 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_chat),
|
||||
stringResource(MR.strings.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = false,
|
||||
disabledLook = false,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -621,7 +690,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
|
||||
withBGApi {
|
||||
val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings)
|
||||
if (success) {
|
||||
ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,7 +723,9 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
|
||||
withBGApi {
|
||||
val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked)
|
||||
chatModel.upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -17,6 +17,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -43,8 +44,10 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
|
||||
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
|
||||
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
|
||||
if (g != null) {
|
||||
m.updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
withChats {
|
||||
updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+4
-1
@@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
@@ -38,7 +39,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
withBGApi {
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
|
||||
if (gInfo != null) {
|
||||
chatModel.updateGroup(rhId, gInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, gInfo)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatJsonLength
|
||||
@@ -52,7 +53,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
||||
val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
|
||||
if (res != null) {
|
||||
gInfo = res
|
||||
m.updateGroup(rhId, res)
|
||||
withChats {
|
||||
updateGroup(rhId, res)
|
||||
}
|
||||
welcomeText.value = welcome ?: ""
|
||||
}
|
||||
afterSave()
|
||||
|
||||
+56
-22
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
@@ -32,7 +34,7 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
@@ -47,6 +49,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
val inProgress = remember { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inProgress.value) {
|
||||
progressByTimeout = if (inProgress.value) {
|
||||
delay(1000)
|
||||
@@ -77,6 +80,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
@@ -96,6 +100,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.Local -> {
|
||||
ChatListNavLinkLayout(
|
||||
@@ -114,6 +119,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
@@ -133,6 +139,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -153,6 +160,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -169,12 +177,13 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorChatListItem() {
|
||||
fun ErrorChatListItem() {
|
||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||
}
|
||||
@@ -471,13 +480,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.accept_contact_button),
|
||||
painterResource(MR.images.ic_check),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -486,7 +495,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -556,7 +565,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
withApi {
|
||||
if (chat.chatStats.unreadCount > 0) {
|
||||
val minUnreadItemId = chat.chatStats.minUnreadItemId
|
||||
chatModel.markChatItemsRead(chat)
|
||||
withChats {
|
||||
markChatItemsRead(chat)
|
||||
}
|
||||
chatModel.controller.apiChatRead(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
@@ -573,7 +584,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
false
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
withChats {
|
||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,12 +604,14 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
true
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
withChats {
|
||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSucess: ((chat: Chat) -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
|
||||
@@ -604,13 +619,13 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@@ -626,13 +641,16 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) {
|
||||
withBGApi {
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
|
||||
if (contact != null && isCurrentUser && contactRequest != null) {
|
||||
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
|
||||
chatModel.replaceChat(rhId, contactRequest.id, chat)
|
||||
withChats {
|
||||
replaceChat(rhId, contactRequest.id, chat)
|
||||
}
|
||||
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
|
||||
close?.invoke(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,7 +658,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
|
||||
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
withBGApi {
|
||||
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
|
||||
chatModel.removeChat(rhId, contactRequest.id)
|
||||
withChats {
|
||||
removeChat(rhId, contactRequest.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,7 +676,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti
|
||||
withBGApi {
|
||||
AlertManager.shared.hideAlert()
|
||||
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
|
||||
chatModel.removeChat(rhId, connection.id)
|
||||
withChats {
|
||||
removeChat(rhId, connection.id)
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@@ -675,7 +697,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(rhId, chatInfo.id)
|
||||
withChats {
|
||||
removeChat(rhId, chatInfo.id)
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -737,7 +761,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean {
|
||||
val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(rhId, contact)
|
||||
withChats {
|
||||
updateContact(rhId, contact)
|
||||
}
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
title = generalGetString(MR.strings.connection_request_sent),
|
||||
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted),
|
||||
@@ -778,7 +804,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(rhId, groupInfo.id)
|
||||
withChats {
|
||||
removeChat(rhId, groupInfo.id)
|
||||
}
|
||||
if (chatModel.chatId.value == groupInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -827,7 +855,9 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
|
||||
else -> false
|
||||
}
|
||||
if (res && newChatInfo != null) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, newChatInfo)
|
||||
withChats {
|
||||
updateChatInfo(chat.remoteHostId, newChatInfo)
|
||||
}
|
||||
if (chatSettings.enableNtfs != MsgFilter.All) {
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
}
|
||||
@@ -848,6 +878,7 @@ expect fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
)
|
||||
|
||||
@Preview/*(
|
||||
@@ -890,7 +921,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -935,7 +967,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -957,7 +990,8 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+166
-77
@@ -12,7 +12,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
@@ -33,7 +33,6 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.group.ProgressIndicator
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -42,28 +41,31 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private fun showNewChatSheet(oneHandUI: State<Boolean>, barTitle: String) {
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.start.showModalCloseable(
|
||||
closeOnTop = !oneHandUI.value,
|
||||
closeBarTitle = if (oneHandUI.value) barTitle else null,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }
|
||||
) { close ->
|
||||
NewChatSheet(rh = chatModel.currentRemoteHost.value, close)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
@@ -77,7 +79,31 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@@ -89,11 +115,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
if (!oneHandUI.state.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
@@ -108,25 +134,33 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier))
|
||||
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
var modifier = Modifier.padding(it).padding(end = endPadding)
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
|
||||
}
|
||||
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
var textModifier = Modifier.align(Alignment.Center)
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
textModifier = textModifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), textModifier, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,17 +169,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
if (appPlatform.isDesktop) {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
if (call != null) {
|
||||
ActiveCallInteractiveArea(call, newChatSheetState)
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
// TODO disable this button and sheet for the duration of the switch
|
||||
tryOrShowError("NewChatSheet", error = {}) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
UserPicker(
|
||||
chatModel = chatModel,
|
||||
userPickerState = userPickerState,
|
||||
contentAlignment = if (oneHandUI.state.value) Alignment.BottomStart else Alignment.TopStart
|
||||
) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
@@ -153,27 +187,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
||||
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
|
||||
val color = MaterialTheme.colors.primaryVariant
|
||||
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {
|
||||
val trianglePath = Path().apply {
|
||||
moveTo(0.dp.toPx(), 0f)
|
||||
lineTo(16.dp.toPx(), 0.dp.toPx())
|
||||
lineTo(8.dp.toPx(), 10.dp.toPx())
|
||||
lineTo(0.dp.toPx(), 0.dp.toPx())
|
||||
}
|
||||
drawPath(
|
||||
color = color,
|
||||
path = trianglePath
|
||||
)
|
||||
})
|
||||
Spacer(Modifier.height(62.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
Button(
|
||||
@@ -191,10 +204,37 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, oneHandUI: SharedPreference<Boolean>) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, shape = CircleShape)
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
){
|
||||
Icon(
|
||||
painterResource(MR.images.ic_edit_filled),
|
||||
stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.size(sp16),
|
||||
tint = if (!stopped) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatingProgress != null) {
|
||||
barButtons.add {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -291,10 +331,12 @@ fun SubscriptionStatusIndicator(click: (() -> Unit)) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun setSubsTotal() {
|
||||
val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId())
|
||||
if (r != null) {
|
||||
subs = r.first
|
||||
hasSess = r.second
|
||||
if (chatModel.currentUser.value != null) {
|
||||
val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId())
|
||||
if (r != null) {
|
||||
subs = r.first
|
||||
hasSess = r.second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +384,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
Text(
|
||||
@@ -377,7 +420,7 @@ private fun ToggleFilterEnabledButton() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||
expect fun ActiveCallInteractiveArea(call: Call)
|
||||
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
@@ -391,11 +434,22 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>, oneHandUI: SharedPreference<Boolean>) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
@@ -415,7 +469,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
}
|
||||
} else {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
@@ -481,9 +535,35 @@ private fun ErrorSettingsView() {
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
enum class ScrollDirection {
|
||||
Up, Down, Idle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>, oneHandUI: SharedPreference<Boolean>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
@@ -494,7 +574,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList())
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
@@ -504,7 +584,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -512,7 +594,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
@@ -520,11 +602,17 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
ChatListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && chatModel.chats.isNotEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
||||
var modifier = Modifier.fillMaxSize();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -543,17 +631,18 @@ private fun filteredChats(
|
||||
} else {
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats
|
||||
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD }
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
|
||||
if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
})
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ fun ChatPreviewView(
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
|
||||
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||
} else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) {
|
||||
if (cInfo.contact.nextSendGrpInv) {
|
||||
|
||||
+3
-1
@@ -719,7 +719,9 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun setServersSummary() {
|
||||
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
|
||||
if (chatModel.currentUser.value != null) {
|
||||
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+15
-7
@@ -6,6 +6,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -21,7 +22,8 @@ fun ShareListNavLinkView(
|
||||
chatModel: ChatModel,
|
||||
isMediaOrFileAttachment: Boolean,
|
||||
isVoice: Boolean,
|
||||
hasSimplexLink: Boolean
|
||||
hasSimplexLink: Boolean,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -29,7 +31,7 @@ fun ShareListNavLinkView(
|
||||
is ChatInfo.Direct -> {
|
||||
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited, oneHandUI = oneHandUI) },
|
||||
click = {
|
||||
if (voiceProhibited) {
|
||||
showForwardProhibitedByPrefAlert()
|
||||
@@ -46,7 +48,7 @@ fun ShareListNavLinkView(
|
||||
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||
val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref, oneHandUI = oneHandUI) },
|
||||
click = {
|
||||
if (prohibitedByPref) {
|
||||
showForwardProhibitedByPrefAlert()
|
||||
@@ -59,7 +61,7 @@ fun ShareListNavLinkView(
|
||||
}
|
||||
is ChatInfo.Local ->
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = false, oneHandUI = oneHandUI) },
|
||||
click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } },
|
||||
stopped
|
||||
)
|
||||
@@ -78,7 +80,7 @@ private fun showForwardProhibitedByPrefAlert() {
|
||||
private fun ShareListNavLinkLayout(
|
||||
chatLinkPreview: @Composable () -> Unit,
|
||||
click: () -> Unit,
|
||||
stopped: Boolean
|
||||
stopped: Boolean,
|
||||
) {
|
||||
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
|
||||
chatLinkPreview()
|
||||
@@ -87,9 +89,15 @@ private fun ShareListNavLinkLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||
private fun SharePreviewView(chat: Chat, disabled: Boolean, oneHandUI: State<Boolean>) {
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
+31
-13
@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -24,13 +25,16 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val (userPickerState, scaffoldState) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
topBar = { if (!oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
bottomBar = { if (oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
val sharedContent = chatModel.sharedContent.value
|
||||
var isMediaOrFileAttachment = false
|
||||
var isVoice = false
|
||||
@@ -56,21 +60,27 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(Modifier.padding(it)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
modifier = modifier
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ShareList(
|
||||
chatModel,
|
||||
search = searchInList,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
} else {
|
||||
EmptyList()
|
||||
EmptyList(oneHandUI = oneHandUI.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,8 +101,14 @@ private fun hasSimplexLink(msg: String): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyList() {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
private fun EmptyList(oneHandUI: State<Boolean>) {
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.you_have_no_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -127,7 +143,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatModel.chats.size >= 8) {
|
||||
if (chatModel.chats.value.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
@@ -182,11 +198,12 @@ private fun ShareList(
|
||||
search: String,
|
||||
isMediaOrFileAttachment: Boolean,
|
||||
isVoice: Boolean,
|
||||
hasSimplexLink: Boolean
|
||||
hasSimplexLink: Boolean,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
val chats by remember(search) {
|
||||
derivedStateOf {
|
||||
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
if (search.isEmpty()) {
|
||||
sorted.filter { it.chatInfo.ready }
|
||||
} else {
|
||||
@@ -203,7 +220,8 @@ private fun ShareList(
|
||||
chatModel,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -40,6 +40,7 @@ fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
showSettings: Boolean = true,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
@@ -149,7 +150,8 @@ fun UserPicker(
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
},
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.ContactType
|
||||
import chat.simplex.common.views.newchat.chatContactType
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private fun onRequestAccepted(chat: Chat) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
ModalManager.start.closeModals()
|
||||
if (chatInfo.contact.sndReady) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val rhId = chat.remoteHostId
|
||||
val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(rhId to chat.chatInfo.id)
|
||||
val contactType = chatContactType(chat)
|
||||
|
||||
LaunchedEffect(chat.id) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
|
||||
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } }
|
||||
val view = LocalMultiplatformView()
|
||||
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
hideKeyboard(view)
|
||||
when (contactType) {
|
||||
ContactType.RECENT -> {
|
||||
withApi {
|
||||
openChat(rhId, chat.chatInfo, chatModel)
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
}
|
||||
ContactType.CHAT_DELETED -> {
|
||||
withApi {
|
||||
openChat(rhId, chat.chatInfo, chatModel)
|
||||
withChats {
|
||||
updateContact(rhId, chat.chatInfo.contact.copy(chatDeleted = false))
|
||||
}
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
}
|
||||
ContactType.CARD -> {
|
||||
askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
chatModel,
|
||||
rhId,
|
||||
chat.chatInfo.contact,
|
||||
close = { ModalManager.start.closeModals() },
|
||||
openChat = true
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
DeleteContactAction(chat, chatModel, showMenu)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
hideKeyboard(view)
|
||||
contactRequestAlertDialog(
|
||||
rhId,
|
||||
chat.chatInfo,
|
||||
chatModel,
|
||||
onSucess = { onRequestAccepted(it) }
|
||||
)
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
ContactRequestMenuItems(
|
||||
rhId = chat.remoteHostId,
|
||||
chatInfo = chat.chatInfo,
|
||||
chatModel = chatModel,
|
||||
showMenu = showMenu,
|
||||
onSuccess = { onRequestAccepted(it) }
|
||||
)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.newchat.ContactType
|
||||
import chat.simplex.common.views.newchat.chatContactType
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ContactPreviewView(
|
||||
chat: Chat,
|
||||
disabled: Boolean,
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
val contactType = chatContactType(chat)
|
||||
|
||||
@Composable
|
||||
fun VerifiedIcon() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chatPreviewTitle() {
|
||||
val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) }
|
||||
|
||||
val textColor = when {
|
||||
deleting -> MaterialTheme.colors.secondary
|
||||
contactType == ContactType.CARD -> MaterialTheme.colors.primary
|
||||
contactType == ContactType.REQUEST -> MaterialTheme.colors.primary
|
||||
contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (cInfo.contact.verified) {
|
||||
VerifiedIcon()
|
||||
}
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(PaddingValues(horizontal = DEFAULT_PADDING_HALF)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
ChatInfoImage(cInfo, size = 42.dp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
|
||||
Box(modifier = Modifier.weight(10f, fill = true)) {
|
||||
chatPreviewTitle()
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
if (chat.chatInfo is ChatInfo.ContactRequest) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(23.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (contactType == ContactType.CARD) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_mail),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(21.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (chat.chatInfo.chatSettings?.favorite == true) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_star_filled),
|
||||
contentDescription = generalGetString(MR.strings.favorite_chat),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(17.dp)
|
||||
)
|
||||
if (chat.chatInfo.incognito) {
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (chat.chatInfo.incognito) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(21.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -502,7 +502,11 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
|
||||
chatModel.chatId.value = null
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chats.clear()
|
||||
withLongRunningApi {
|
||||
withChats {
|
||||
chats.clear()
|
||||
}
|
||||
}
|
||||
chatModel.users.clear()
|
||||
ntfManager.cancelAllNotifications()
|
||||
}
|
||||
@@ -714,10 +718,10 @@ private fun afterSetCiTTL(
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
|
||||
withApi {
|
||||
try {
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
// this is using current remote host on purpose - if it changes during update, it will load correct chats
|
||||
val chats = m.controller.apiGetChats(m.remoteHostId())
|
||||
m.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "apiGetChats error: ${e.message}")
|
||||
|
||||
+27
-4
@@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -18,17 +20,26 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
var rowModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
rowModifier = rowModifier.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
verticalArrangement = arrangement,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.padding(horizontal = AppBarHorizontalPadding)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = AppBarHorizontalPadding),
|
||||
content = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
rowModifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -37,6 +48,18 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
||||
} else {
|
||||
Spacer(Modifier)
|
||||
}
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
closeBarTitle,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
navigationButton: @Composable RowScope.() -> Unit,
|
||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||
title: (@Composable () -> Unit)?,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
showSearch: Boolean,
|
||||
@@ -126,5 +126,6 @@ private fun TopAppBar(
|
||||
|
||||
val AppBarHeight = 56.dp
|
||||
val AppBarHorizontalPadding = 4.dp
|
||||
val BottomAppBarHeight = 60.dp
|
||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
||||
val TitleInsetWithIcon = 72.dp
|
||||
|
||||
+19
-6
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
@@ -24,6 +25,8 @@ fun ModalView(
|
||||
enableClose: Boolean = true,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
closeOnTop: Boolean = true,
|
||||
closeBarTitle: String? = null,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
@@ -32,8 +35,16 @@ fun ModalView(
|
||||
}
|
||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
Box(modifier) { content() }
|
||||
if (closeOnTop) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
}
|
||||
Box(if (closeOnTop) modifier else modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
if (!closeOnTop) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons, arrangement = Arrangement.Bottom, closeBarTitle = closeBarTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,17 +71,17 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null,endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content() })
|
||||
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, endButtons = endButtons, content = { data.content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, content = { data.content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +116,8 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
val hasModalsOpen: Boolean
|
||||
@Composable get () = remember { modalCount }.value > 0
|
||||
|
||||
fun openModalCount() = modalCount.value
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
|
||||
+10
-6
@@ -18,6 +18,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.common.views.chatlist.setGroupMembers
|
||||
@@ -32,19 +33,22 @@ import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) {
|
||||
val rhId = rh?.remoteHostId
|
||||
AddGroupLayout(
|
||||
createGroup = { incognito, groupProfile ->
|
||||
withBGApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||
if (groupInfo != null) {
|
||||
chatModel.updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
withChats {
|
||||
updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
}
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
close.invoke()
|
||||
closeAll.invoke()
|
||||
|
||||
if (!groupInfo.incognito) {
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close)
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -331,7 +332,9 @@ suspend fun connectViaUri(
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
if (pcc != null) {
|
||||
chatModel.updateContactConnection(rhId, pcc)
|
||||
withChats {
|
||||
updateContactConnection(rhId, pcc)
|
||||
}
|
||||
close?.invoke()
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
title = generalGetString(MR.strings.connection_request_sent),
|
||||
|
||||
+4
-1
@@ -22,6 +22,7 @@ import chat.simplex.common.views.chat.LocalAliasEditor
|
||||
import chat.simplex.common.views.chatlist.deleteContactConnectionAlert
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.PendingContactConnection
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -186,7 +187,9 @@ fun DeleteButton(onClick: () -> Unit) {
|
||||
|
||||
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
|
||||
chatModel.updateContactConnection(rhId, it)
|
||||
withChats {
|
||||
updateContactConnection(rhId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+550
-137
@@ -1,177 +1,597 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.ScrollDirection
|
||||
import chat.simplex.common.views.contacts.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
// TODO close new chat if remote host changes in model
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
stopped,
|
||||
addContact = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||
},
|
||||
scanPaste = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||
},
|
||||
closeNewChatSheet,
|
||||
)
|
||||
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.new_chat),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val closeAll = { ModalManager.start.closeModals() }
|
||||
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
NewChatSheetLayout(
|
||||
addContact = {
|
||||
ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) }
|
||||
},
|
||||
scanPaste = {
|
||||
ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) }
|
||||
},
|
||||
createGroup = {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val titles = listOf(
|
||||
MR.strings.add_contact_tab,
|
||||
MR.strings.scan_paste_link,
|
||||
MR.strings.create_group_button
|
||||
)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||
enum class ContactType {
|
||||
CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED
|
||||
}
|
||||
|
||||
fun chatContactType(chat: Chat): ContactType {
|
||||
return when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.ContactRequest -> ContactType.REQUEST
|
||||
is ChatInfo.Direct -> {
|
||||
val contact = cInfo.contact;
|
||||
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
|
||||
contact.chatDeleted -> ContactType.CHAT_DELETED
|
||||
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
|
||||
else -> ContactType.UNLISTED
|
||||
}
|
||||
}
|
||||
else -> ContactType.UNLISTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>): List<Chat> {
|
||||
return c.filter { chat -> contactTypes.contains(chatContactType(chat)) }
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
rh: RemoteHostInfo?,
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||
close: () -> Unit,
|
||||
oneHandUI: SharedPreference<Boolean>
|
||||
) {
|
||||
var newChat by remember { mutableStateOf(newChatSheetState.value) }
|
||||
val resultingColor = if (isInDarkTheme()) Color.Black.copy(0.64f) else DrawerDefaults.scrimColor
|
||||
val animatedColor = remember {
|
||||
Animatable(
|
||||
if (newChat.isVisible()) Color.Transparent else resultingColor,
|
||||
Color.VectorConverter(resultingColor.colorSpace)
|
||||
)
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val baseContactTypes = listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST)
|
||||
val contactTypes by remember(baseContactTypes, searchText.value.text.isEmpty()) {
|
||||
derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) }
|
||||
}
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
newChatSheetState.collect {
|
||||
newChat = it
|
||||
launch {
|
||||
animatedColor.animateTo(if (newChat.isVisible()) resultingColor else Color.Transparent, newChatSheetAnimSpec())
|
||||
}
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) closeNewChatSheet(false)
|
||||
val allChats by remember(chatModel.chats.value, contactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) }
|
||||
}
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
var sectionModifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
sectionModifier = sectionModifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
stickyHeader {
|
||||
Column(
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = endPadding)
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) }
|
||||
.drawBehind { drawRect(animatedColor.value) },
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||
val backgroundColor = if (isInDarkTheme())
|
||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||
else
|
||||
MaterialTheme.colors.background
|
||||
LazyColumn(Modifier
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (1 - animatedFloat.value) * 20.dp.toPx()
|
||||
}) {
|
||||
items(actions.size) { index ->
|
||||
item {
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Row {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(contentAlignment = Alignment.CenterEnd) {
|
||||
Button(
|
||||
actions[index],
|
||||
shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier),
|
||||
colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor),
|
||||
elevation = null,
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF),
|
||||
modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
Text(
|
||||
stringResource(titles[index]),
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF),
|
||||
color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Icon(
|
||||
painterResource(icons[index]),
|
||||
stringResource(titles[index]),
|
||||
Modifier.size(42.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary
|
||||
)
|
||||
SectionView {
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_add_link),
|
||||
text = stringResource(MR.strings.add_contact_tab),
|
||||
click = addContact,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_qr_code),
|
||||
text = if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
|
||||
click = scanPaste,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_group),
|
||||
text = stringResource(MR.strings.create_group_button),
|
||||
click = createGroup,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
Row(modifier = sectionModifier) {
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.state.value,
|
||||
closeBarTitle = if (oneHandUI.state.value) generalGetString(MR.strings.deleted_chats) else null,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }
|
||||
) {
|
||||
DeletedContactsView(rh = rh, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_folder_open),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(extraPadding = true)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { if (!stopped) closeNewChatSheet(true) },
|
||||
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
),
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && !oneHandUI.state.value) {
|
||||
Text(
|
||||
stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewChatButton(
|
||||
icon: Painter,
|
||||
text: String,
|
||||
click: () -> Unit,
|
||||
textColor: Color = Color.Unspecified,
|
||||
iconColor: Color = MaterialTheme.colors.secondary,
|
||||
disabled: Boolean = false,
|
||||
extraPadding: Boolean = false,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
SectionItemView(click, disabled = disabled) {
|
||||
Row(modifier = if (oneHandUI.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier) {
|
||||
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor)
|
||||
TextIconSpaced(extraPadding)
|
||||
Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactsSearchBar(
|
||||
listState: LazyListState,
|
||||
searchText: MutableState<TextFieldValue>,
|
||||
searchShowingSimplexLink: MutableState<Boolean>,
|
||||
searchChatFilteredBySimplexLink: MutableState<String?>,
|
||||
close: () -> Unit,
|
||||
oneHandUI: SharedPreference<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
trailingContent = null,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText =
|
||||
link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||
searchText.value =
|
||||
searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(
|
||||
link = link.text,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
cleanup = { searchText.value = TextFieldValue() }
|
||||
)
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterButton() {
|
||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(sp16)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, close: () -> Unit, cleanup: (() -> Unit)?) {
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
close = close,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredContactChats(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchShowingSimplexLink: State<Boolean>,
|
||||
searchChatFilteredBySimplexLink: State<String?>,
|
||||
searchText: String,
|
||||
contactChats: List<Chat>
|
||||
): List<Chat> {
|
||||
val linkChatId = searchChatFilteredBySimplexLink.value
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
|
||||
return if (linkChatId != null) {
|
||||
contactChats.filter { it.id == linkChatId }
|
||||
} else {
|
||||
contactChats.filter { chat ->
|
||||
filterChat(
|
||||
chat = chat,
|
||||
searchText = s,
|
||||
showUnreadAndFavorites = showUnreadAndFavorites
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(chatsByTypeComparator)
|
||||
}
|
||||
|
||||
private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean {
|
||||
var meetsPredicate = true;
|
||||
val s = searchText.trim().lowercase()
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
if (searchText.isNotEmpty()) {
|
||||
meetsPredicate = viewNameContains(cInfo, s) ||
|
||||
if (cInfo is ChatInfo.Direct) (cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s)) else false
|
||||
}
|
||||
|
||||
if (showUnreadAndFavorites) {
|
||||
meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false)
|
||||
}
|
||||
|
||||
return meetsPredicate;
|
||||
}
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
private val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 ->
|
||||
val chat1Type = chatContactType(chat1)
|
||||
val chat2Type = chatContactType(chat2)
|
||||
|
||||
when {
|
||||
chat1Type.ordinal < chat2Type.ordinal -> -1
|
||||
chat1Type.ordinal > chat2Type.ordinal -> 1
|
||||
|
||||
else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searchEmpty: Boolean): List<ContactType> {
|
||||
return if (baseContactTypes.contains(ContactType.CHAT_DELETED) || searchEmpty) {
|
||||
baseContactTypes
|
||||
} else {
|
||||
baseContactTypes + ContactType.CHAT_DELETED
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeletedContactsView(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier,
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val contactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val allChats by remember(chatModel.chats.value, contactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
item {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
Divider()
|
||||
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = if (oneHandUI.state.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -267,13 +687,6 @@ fun ActionButton(
|
||||
@Composable
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheetLayout(
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
scanPaste = {},
|
||||
createGroup = {},
|
||||
closeNewChatSheet = {},
|
||||
)
|
||||
NewChatSheet(rh = null, close = {})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -25,6 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -62,7 +63,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
* It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore]
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) {
|
||||
if (chatModel.showingInvitation.value != null && (ModalManager.start.openModalCount() == 1 || appPlatform.isDesktop)) {
|
||||
val conn = contactConnection.value
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -218,8 +219,10 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
|
||||
withBGApi {
|
||||
val contactConn = contactConnection.value ?: return@withBGApi
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
|
||||
contactConnection.value = conn
|
||||
chatModel.updateContactConnection(rhId, conn)
|
||||
withChats {
|
||||
contactConnection.value = conn
|
||||
updateContactConnection(rhId, conn)
|
||||
}
|
||||
}
|
||||
chatModel.markShowingInvitationUsed()
|
||||
}
|
||||
@@ -234,7 +237,8 @@ private fun AddContactLearnMoreButton() {
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
AddContactLearnMore(close)
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.size(18.dp * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
@@ -367,9 +371,11 @@ private fun createInvitation(
|
||||
withBGApi {
|
||||
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
|
||||
if (r != null) {
|
||||
chatModel.updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
contactConnection.value = r.second
|
||||
withChats {
|
||||
updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
contactConnection.value = r.second
|
||||
}
|
||||
} else {
|
||||
creatingConnReq.value = false
|
||||
if (alert != null) {
|
||||
|
||||
+4
-1
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
|
||||
if (updated != null) {
|
||||
val (updatedProfile, updatedContacts) = updated
|
||||
m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences)
|
||||
updatedContacts.forEach { m.updateContact(user.remoteHostId, it) }
|
||||
withChats {
|
||||
updatedContacts.forEach { updateContact(user.remoteHostId, it) }
|
||||
}
|
||||
currentPreferences = preferences
|
||||
}
|
||||
afterSave()
|
||||
|
||||
+24
-19
@@ -32,6 +32,7 @@ import chat.simplex.common.views.isValidDisplayName
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -121,14 +122,16 @@ fun PrivacySettingsView(
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
var contact = chat.chatInfo.contact
|
||||
val sendRcpts = contact.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateContact(currentUser.remoteHostId, contact)
|
||||
withChats {
|
||||
for (i in 0 until chats.size) {
|
||||
val chat = chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
var contact = chat.chatInfo.contact
|
||||
val sendRcpts = contact.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
|
||||
updateContact(currentUser.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,15 +146,17 @@ fun PrivacySettingsView(
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateGroup(currentUser.remoteHostId, groupInfo)
|
||||
withChats {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chats.size) {
|
||||
val chat = chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
updateGroup(currentUser.remoteHostId, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +169,7 @@ fun PrivacySettingsView(
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
@@ -179,7 +184,7 @@ fun PrivacySettingsView(
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Settings</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="member_contact_send_direct_message">send direct message</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
@@ -442,10 +443,25 @@
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="info_view_connect_button">connect</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_call_button">call</string>
|
||||
<string name="info_view_search_button">search</string>
|
||||
<string name="info_view_video_button">video</string>
|
||||
<string name="delete_contact_question">Delete contact?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string>
|
||||
<string name="keep_conversation">Keep conversation</string>
|
||||
<string name="only_delete_conversation">Only delete conversation</string>
|
||||
<string name="confirm_delete_contact_question">Confirm contact deletion?</string>
|
||||
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
||||
<string name="delete_without_notification">Delete without notification</string>
|
||||
<string name="button_delete_contact">Delete contact</string>
|
||||
<string name="conversation_deleted">Conversation deleted!</string>
|
||||
<string name="you_can_still_send_messages_to_contact">You can still send messages to %1$s from the Deleted chats.</string>
|
||||
<string name="contact_deleted">Contact deleted!</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the list of chats.</string>
|
||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
@@ -633,6 +649,7 @@
|
||||
<string name="new_chat">New chat</string>
|
||||
<string name="add_contact_tab">Add contact</string>
|
||||
<string name="scan_paste_link">Scan / Paste link</string>
|
||||
<string name="paste_link">Paste link</string>
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<string name="one_time_link_short">1-time link</string>
|
||||
<string name="simplex_address">SimpleX address</string>
|
||||
@@ -651,6 +668,10 @@
|
||||
<string name="invalid_qr_code">Invalid QR code</string>
|
||||
<string name="code_you_scanned_is_not_simplex_link_qr_code">The code you scanned is not a SimpleX link QR code.</string>
|
||||
|
||||
<string name="deleted_chats">Deleted chats</string>
|
||||
<string name="no_filtered_contacts">No filtered contacts</string>
|
||||
<string name="contact_list_header_title">Your contacts</string>
|
||||
|
||||
<!-- ScanCodeView.kt -->
|
||||
<string name="scan_code">Scan code</string>
|
||||
<string name="incorrect_code">Incorrect security code!</string>
|
||||
@@ -1123,6 +1144,7 @@
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
<string name="settings_experimental_features">Experimental features</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_interface" translatable="false">INTERFACE</string>
|
||||
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
@@ -1258,6 +1280,7 @@
|
||||
<string name="database_downgrade">Database downgrade</string>
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="one_hand_ui">One-hand UI</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
@@ -1450,6 +1473,7 @@
|
||||
<string name="send_receipts_disabled">disabled</string>
|
||||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
<string name="action_button_add_members">Invite</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -1527,6 +1551,17 @@
|
||||
<string name="message_queue_info_none">none</string>
|
||||
<string name="message_queue_info_server_info">server queue info: %1$s\n\nlast received msg: %2$s</string>
|
||||
|
||||
<string name="cant_call_contact_alert_title">Can\'t call contact</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Connecting to contact, please wait or check later!</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">Contact is deleted.</string>
|
||||
<string name="allow_calls_question">Allow calls?</string>
|
||||
<string name="you_need_to_allow_calls">You need to allow your contact to call to be able to call them.</string>
|
||||
<string name="calls_prohibited_alert_title">Calls prohibited!</string>
|
||||
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Please ask your contact to enable calls.</string>
|
||||
<string name="cant_call_member_alert_title">Can\'t call group member</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Send message to enable calls.</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Can\'t message group member</string>
|
||||
|
||||
<!-- GroupWelcomeView.kt -->
|
||||
<string name="group_welcome_title">Welcome message</string>
|
||||
<string name="save_welcome_message_question">Save welcome message?</string>
|
||||
|
||||
+5
-3
@@ -52,9 +52,11 @@ actual fun LazyColumnWithScrollBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout)
|
||||
Box {
|
||||
LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>,
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!disabled) modifier = modifier
|
||||
|
||||
+57
-43
@@ -22,53 +22,67 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||
// if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
if (!newChatSheetState.collectAsState().value.isVisible()) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withBGApi {
|
||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||
}
|
||||
Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) {
|
||||
if (media == CallMediaType.Video) {
|
||||
Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White)
|
||||
} else {
|
||||
Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White)
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withBGApi {
|
||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||
}
|
||||
Box(
|
||||
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
if (media == CallMediaType.Video) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam_filled),
|
||||
stringResource(MR.strings.icon_descr_video_call),
|
||||
Modifier.size(18.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_filled),
|
||||
stringResource(MR.strings.icon_descr_audio_call),
|
||||
Modifier.size(18.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.icon_descr_hang_up),
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
color = MaterialTheme.colors.error,
|
||||
onClick = {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.size
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.DesktopPlatform
|
||||
import chat.simplex.common.showApp
|
||||
|
||||
@@ -40,6 +40,10 @@ if [ ! -f ../appimagetool-x86_64.AppImage ]; then
|
||||
wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage
|
||||
chmod +x ../appimagetool-x86_64.AppImage
|
||||
fi
|
||||
../appimagetool-x86_64.AppImage .
|
||||
if [ ! -f ../runtime-fuse3-x86_64 ]; then
|
||||
wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/old/runtime-fuse3-x86_64 -O ../runtime-fuse3-x86_64
|
||||
chmod +x ../runtime-fuse3-x86_64
|
||||
fi
|
||||
../appimagetool-x86_64.AppImage --runtime-file ../runtime-fuse3-x86_64 .
|
||||
|
||||
mv *imple*.AppImage ../../
|
||||
|
||||
@@ -1745,7 +1745,7 @@ markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta
|
||||
WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(DBCIDeleted, deletedTs, currentTs, userId, contactId, itemId)
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs}}
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs, editable = False, deletable = False}}
|
||||
|
||||
getDirectChatItemBySharedMsgId :: DB.Connection -> User -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect)
|
||||
getDirectChatItemBySharedMsgId db user@User {userId} contactId sharedMsgId = do
|
||||
@@ -1945,7 +1945,7 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta}
|
||||
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId)
|
||||
pure ci {meta = meta {itemDeleted}}
|
||||
pure ci {meta = meta {itemDeleted, editable = False, deletable = False}}
|
||||
|
||||
markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv)
|
||||
markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do
|
||||
@@ -1958,7 +1958,7 @@ markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta}
|
||||
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(DBCIBlocked, deletedTs, deletedTs, userId, groupId, chatItemId' ci)
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs}}
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs, editable = False, deletable = False}}
|
||||
|
||||
markGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv)
|
||||
markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do
|
||||
@@ -1971,7 +1971,7 @@ markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta
|
||||
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(DBCIBlockedByAdmin, deletedTs, deletedTs, userId, groupId, chatItemId' ci)
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs}}
|
||||
pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs, editable = False, deletable = False}}
|
||||
|
||||
getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup)
|
||||
getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"signing-key-fingerprint": "Otisk podpisového klíče (SHA-256)",
|
||||
"f-droid-org-repo": "F-Droid.org repozitář",
|
||||
"stable-versions-built-by-f-droid-org": "Stabilní verze vytvořené F-Droid.org",
|
||||
"releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o 1-2 dny později",
|
||||
"releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o několik dní později",
|
||||
"jobs": "Připojit k týmu",
|
||||
"hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.",
|
||||
"hero-overlay-card-3-p-3": "Přečtěte si více v <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ohlášení</a>.",
|
||||
|
||||
Reference in New Issue
Block a user