Compare commits

..

1 Commits

Author SHA1 Message Date
Evgeny Poberezkin
0366afbcce rfc: user per-service per-device identities 2023-07-02 14:03:55 +01:00
595 changed files with 6538 additions and 16940 deletions

View File

@@ -14,28 +14,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
if #available(iOS 17.0, *) { trackKeyboard() }
return true
}
@available(iOS 17.0, *)
private func trackKeyboard() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@available(iOS 17.0, *)
@objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height
}
}
@available(iOS 17.0, *)
@objc func keyboardWillHide(_ notification: Notification) {
ChatModel.shared.keyboardHeight = 0
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")

View File

@@ -57,8 +57,6 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
// tracks keyboard height via subscription in AppDelegate
@Published var keyboardHeight: CGFloat = 0
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -135,14 +133,6 @@ final class ChatModel: ObservableObject {
updateChat(.direct(contact: contact), addMissing: contact.directOrUsed)
}
func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) {
var updatedConn = contact.activeConn
updatedConn.connectionStats = connectionStats
var updatedContact = contact
updatedContact.activeConn = updatedConn
updateContact(updatedContact)
}
func updateGroup(_ groupInfo: GroupInfo) {
updateChat(.group(groupInfo: groupInfo))
}
@@ -531,16 +521,6 @@ final class ChatModel: ObservableObject {
}
}
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
if let conn = member.activeConn {
var updatedConn = conn
updatedConn.connectionStats = connectionStats
var updatedMember = member
updatedMember.activeConn = updatedConn
_ = upsertGroupMember(groupInfo, updatedMember)
}
}
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
var i = 0
var totalBelow = 0

View File

@@ -464,10 +464,6 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
throw r
}
func reconnectAllServers() async throws {
try await sendCommandOkResp(.reconnectAllServers)
}
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
}
@@ -478,9 +474,9 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
throw r
}
@@ -508,18 +504,6 @@ func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws
throw r
}
func apiSyncContactRatchet(_ contactId: Int64, _ force: Bool) throws -> ConnectionStats {
let r = chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force))
if case let .contactRatchetSyncStarted(_, _, connectionStats) = r { return connectionStats }
throw r
}
func apiSyncGroupMemberRatchet(_ groupId: Int64, _ groupMemberId: Int64, _ force: Bool) throws -> (GroupMember, ConnectionStats) {
let r = chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force))
if case let .groupMemberRatchetSyncStarted(_, _, member, connectionStats) = r { return (member, connectionStats) }
throw r
}
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
@@ -1465,14 +1449,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
case .chatSuspended:
chatSuspended()
case let .contactSwitch(_, contact, switchProgress):
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
default:
logger.debug("unsupported event: \(res.responseType)")
}

View File

@@ -76,7 +76,6 @@ struct ChatInfoView: View {
case networkStatusAlert
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
@@ -86,7 +85,6 @@ struct ChatInfoView: View {
case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
case let .error(title, _): return "error \(title)"
}
}
@@ -117,12 +115,6 @@ struct ChatInfoView: View {
Section {
if let code = connectionCode { verifyCodeButton(code) }
contactPreferencesButton()
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
} else if developerTools {
synchronizeConnectionButtonForce()
}
}
if let contactLink = contact.contactLink {
@@ -149,18 +141,12 @@ struct ChatInfoView: View {
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
@@ -189,7 +175,6 @@ struct ChatInfoView: View {
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
@@ -295,24 +280,6 @@ struct ChatInfoView: View {
}
}
private func synchronizeConnectionButton() -> some View {
Button {
syncContactConnection(force: false)
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
}
}
private func synchronizeConnectionButtonForce() -> some View {
Button {
alert = .syncConnectionForceAlert
} label: {
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
.foregroundColor(.red)
}
}
private func networkStatusRow() -> some View {
HStack {
Text("Network status")
@@ -403,10 +370,6 @@ struct ChatInfoView: View {
do {
let stats = try apiSwitchContact(contactId: contact.apiId)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
@@ -422,9 +385,6 @@ struct ChatInfoView: View {
do {
let stats = try apiAbortSwitchContact(contact.apiId)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
}
} catch let error {
logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
@@ -434,25 +394,6 @@ struct ChatInfoView: View {
}
}
}
private func syncContactConnection(force: Bool) {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
@@ -473,15 +414,6 @@ func abortSwitchAddressAlert(_ abortSwitchAddress: @escaping () -> Void) -> Aler
)
}
func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Alert {
Alert(
title: Text("Renegotiate encryption?"),
message: Text("The encryption is working and the new encryption agreement is not required. It may result in connection errors!"),
primaryButton: .destructive(Text("Renegotiate"), action: syncConnectionForce),
secondaryButton: .cancel()
)
}
struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View {
ChatInfoView(

View File

@@ -12,215 +12,25 @@ import SimpleXChat
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
struct CIRcvDecryptionError: View {
@EnvironmentObject var chat: Chat
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
var chatItem: ChatItem
var showMember = false
@State private var alert: CIRcvDecryptionErrorAlert?
enum CIRcvDecryptionErrorAlert: Identifiable {
case syncAllowedAlert(_ syncConnection: () -> Void)
case syncNotSupportedContactAlert
case syncNotSupportedMemberAlert
case decryptionErrorAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String {
switch self {
case .syncAllowedAlert: return "syncAllowedAlert"
case .syncNotSupportedContactAlert: return "syncNotSupportedContactAlert"
case .syncNotSupportedMemberAlert: return "syncNotSupportedMemberAlert"
case .decryptionErrorAlert: return "decryptionErrorAlert"
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
viewBody()
.onAppear {
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, member, s)
}
} catch let error {
logger.error("apiGroupMemberInfo error: \(responseError(error))")
}
}
CIMsgError(chatItem: chatItem, showMember: showMember) {
var message: Text
let why = Text(decryptErrorReason)
let permanent = Text("This error is permanent for this connection, please re-connect.")
switch msgDecryptError {
case .ratchetHeader:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + Text("\n") + permanent
case .tooManySkipped:
message = Text("\(msgCount) messages skipped.") + Text("\n") + why + Text("\n") + permanent
}
.alert(item: $alert) { alertItem in
switch(alertItem) {
case let .syncAllowedAlert(syncConnection): return syncAllowedAlert(syncConnection)
case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message())
case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message())
case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message())
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
}
}
@ViewBuilder private func viewBody() -> some View {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
}
} else {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
let memberStats = modelMember.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
AlertManager.shared.showAlert(Alert(title: Text("Decryption error"), message: message))
}
}
private func basicDecryptionErrorItem() -> some View {
decryptionErrorItem { alert = .decryptionErrorAlert }
}
private func decryptionErrorItemFixButton(syncSupported: Bool, _ onClick: @escaping (() -> Void)) -> some View {
ZStack(alignment: .bottomTrailing) {
VStack(alignment: .leading, spacing: 2) {
HStack {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
}
(
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ Text("Fix connection")
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
)
}
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View {
func text() -> Text {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
}
return ZStack(alignment: .bottomTrailing) {
HStack {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ") + text()
} else {
text()
}
}
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
}
private func message() -> Text {
var message: Text
let why = Text(decryptErrorReason)
switch msgDecryptError {
case .ratchetHeader:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .tooManySkipped:
message = Text("\(msgCount) messages skipped.") + Text("\n") + why
case .ratchetEarlier:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .other:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
}
return message
}
private func syncMemberConnection(_ groupInfo: GroupInfo, _ member: GroupMember) {
Task {
do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false)
await MainActor.run {
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, mem, stats)
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
private func syncContactConnection(_ contact: Contact) {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, false)
await MainActor.run {
ChatModel.shared.updateContactConnectionStats(contact, stats)
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
private func syncAllowedAlert(_ syncConnection: @escaping () -> Void) -> Alert {
Alert(
title: Text("Fix connection?"),
message: message(),
primaryButton: .default(Text("Fix"), action: syncConnection),
secondaryButton: .cancel()
)
}
}
//struct CIRcvDecryptionError_Previews: PreviewProvider {

View File

@@ -22,7 +22,7 @@ struct ChatView: View {
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil
@State private var keyboardVisible = false
@FocusState private var keyboardVisible: Bool
@State private var showDeleteMessage = false
@State private var connectionStats: ConnectionStats?
@State private var customUserProfile: Profile?
@@ -39,16 +39,6 @@ struct ChatView: View {
@State private var selectedMember: GroupMember? = nil
var body: some View {
if #available(iOS 16.0, *) {
viewBody
.scrollDismissesKeyboard(.immediately)
.keyboardPadding()
} else {
viewBody
}
}
private var viewBody: some View {
let cInfo = chat.chatInfo
return VStack(spacing: 0) {
if searchMode {
@@ -75,14 +65,17 @@ struct ChatView: View {
.navigationTitle(cInfo.chatViewName)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
initChatView()
}
.onChange(of: chatModel.chatId) { cId in
if cId != nil {
initChatView()
} else {
dismiss()
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
}
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
.onChange(of: chatModel.chatId) { _ in
if chatModel.chatId == nil { dismiss() }
}
.onDisappear {
VideoPlayerView.players.removeAll()
@@ -192,32 +185,6 @@ struct ChatView: View {
}
}
private func initChatView() {
let cInfo = chat.chatInfo
if case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
await MainActor.run {
if let s = stats {
chatModel.updateContactConnectionStats(contact, s)
}
}
} catch let error {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
}
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
}
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
private func searchToolbar() -> some View {
HStack {
HStack {
@@ -649,7 +616,7 @@ struct ChatView: View {
private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu {
UIMenu(
title: NSLocalizedString("React", comment: "chat item menu"),
title: NSLocalizedString("React...", comment: "chat item menu"),
image: UIImage(systemName: "face.smiling"),
children: rs
)

View File

@@ -234,7 +234,7 @@ struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
@Binding var composeState: ComposeState
@Binding var keyboardVisible: Bool
@FocusState.Binding var keyboardVisible: Bool
@State var linkUrl: URL? = nil
@State var prevLinkUrl: URL? = nil
@@ -943,18 +943,19 @@ struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
@State var composeState = ComposeState(message: "hello")
@FocusState var keyboardVisible: Bool
return Group {
ComposeView(
chat: chat,
composeState: $composeState,
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
.environmentObject(ChatModel())
ComposeView(
chat: chat,
composeState: $composeState,
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
.environmentObject(ChatModel())
}

View File

@@ -16,7 +16,7 @@ struct NativeTextEditor: UIViewRepresentable {
@Binding var disableEditing: Bool
let height: CGFloat
let font: UIFont
@Binding var focused: Bool
@FocusState.Binding var focused: Bool
let alignment: TextAlignment
let onImagesAdded: ([UploadContent]) -> Void
@@ -144,12 +144,13 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
struct NativeTextEditor_Previews: PreviewProvider{
static var previews: some View {
@FocusState var keyboardVisible: Bool
return NativeTextEditor(
text: Binding.constant("Hello, world!"),
disableEditing: Binding.constant(false),
height: 100,
font: UIFont.preferredFont(forTextStyle: .body),
focused: Binding.constant(false),
focused: $keyboardVisible,
alignment: TextAlignment.leading,
onImagesAdded: { _ in }
)

View File

@@ -27,7 +27,7 @@ struct SendMessageView: View {
var onMediaAdded: ([UploadContent]) -> Void
@State private var holdingVMR = false
@Namespace var namespace
@Binding var keyboardVisible: Bool
@FocusState.Binding var keyboardVisible: Bool
@State private var teHeight: CGFloat = 42
@State private var teFont: Font = .body
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
@@ -401,6 +401,7 @@ struct SendMessageView_Previews: PreviewProvider {
@State var composeStateNew = ComposeState()
let ci = ChatItem.getSample(1, .directSnd, .now, "hello")
@State var composeStateEditing = ComposeState(editingItem: ci)
@FocusState var keyboardVisible: Bool
@State var sendEnabled: Bool = true
return Group {
@@ -411,7 +412,7 @@ struct SendMessageView_Previews: PreviewProvider {
composeState: $composeStateNew,
sendMessage: { _ in },
onMediaAdded: { _ in },
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
}
VStack {
@@ -421,7 +422,7 @@ struct SendMessageView_Previews: PreviewProvider {
composeState: $composeStateEditing,
sendMessage: { _ in },
onMediaAdded: { _ in },
keyboardVisible: Binding.constant(true)
keyboardVisible: $keyboardVisible
)
}
}

View File

@@ -126,7 +126,6 @@ struct GroupChatInfoView: View {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
}
}
.keyboardPadding()
}
private func groupInfoHeader() -> some View {

View File

@@ -27,7 +27,6 @@ struct GroupMemberInfoView: View {
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
case abortSwitchAddressAlert
case syncConnectionForceAlert
case connRequestSentAlert(type: ConnReqType)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
case other(alert: Alert)
@@ -39,7 +38,6 @@ struct GroupMemberInfoView: View {
case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .connRequestSentAlert: return "connRequestSentAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
case let .error(title, _): return "error \(title)"
case let .other(alert): return "other \(alert)"
}
@@ -79,12 +77,6 @@ struct GroupMemberInfoView: View {
}
}
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
} else if developerTools {
synchronizeConnectionButtonForce()
}
}
}
@@ -137,18 +129,12 @@ struct GroupMemberInfoView: View {
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil })
if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
.disabled(connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch })
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
@@ -176,7 +162,7 @@ struct GroupMemberInfoView: View {
}
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let stats = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
member = mem
connectionStats = stats
@@ -199,7 +185,6 @@ struct GroupMemberInfoView: View {
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
case let .connRequestSentAlert(type): return connReqSentAlert(type)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
case let .other(alert): return alert
@@ -306,24 +291,7 @@ struct GroupMemberInfoView: View {
systemImage: member.verified ? "checkmark.shield" : "shield"
)
}
}
private func synchronizeConnectionButton() -> some View {
Button {
syncMemberConnection(force: false)
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
}
}
private func synchronizeConnectionButtonForce() -> some View {
Button {
alert = .syncConnectionForceAlert
} label: {
Label("Renegotiate encryption", systemImage: "exclamationmark.triangle")
.foregroundColor(.red)
}
}
private func removeMemberButton(_ mem: GroupMember) -> some View {
@@ -389,11 +357,7 @@ struct GroupMemberInfoView: View {
Task {
do {
let stats = try apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
dismiss()
}
connectionStats = stats
} catch let error {
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
@@ -409,9 +373,6 @@ struct GroupMemberInfoView: View {
do {
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
}
} catch let error {
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
@@ -421,25 +382,6 @@ struct GroupMemberInfoView: View {
}
}
}
private func syncMemberConnection(force: Bool) {
Task {
do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats)
dismiss()
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider {

View File

@@ -18,14 +18,6 @@ struct ChatListView: View {
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
if #available(iOS 16.0, *) {
viewBody.scrollDismissesKeyboard(.immediately)
} else {
viewBody
}
}
private var viewBody: some View {
ZStack(alignment: .topLeading) {
NavStackCompat(
isActive: Binding(
@@ -63,40 +55,12 @@ struct ChatListView: View {
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.onAppear() { connectViaUrl() }
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"),
message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."),
primaryButton: .default(Text("Ok")) {
Task {
do {
try await reconnectAllServers()
} catch let error {
AlertManager.shared.showAlertMsg(title: "Error", message: "\(responseError(error))")
}
}
},
secondaryButton: .cancel()
))
}
.offset(x: -8)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
.frame(width: 32, height: 32)
.padding(.trailing, 4)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
if !allRead {
unreadBadge(size: 12)
}
}
.onTapGesture {
Button {
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
withAnimation {
userPickerVisible.toggle()
@@ -104,6 +68,19 @@ struct ChatListView: View {
} else {
showSettings = true
}
} label: {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
.frame(width: 32, height: 32)
.padding(.trailing, 4)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
if !allRead {
unreadBadge(size: 12)
}
}
}
}
ToolbarItem(placement: .principal) {

View File

@@ -65,7 +65,7 @@ struct MigrateToAppGroupView: View {
case .exporting:
center {
ProgressView(value: 0.33)
Text("Exporting database archive")
Text("Exporting database archive...")
}
migrationProgress()
case .export_error:
@@ -82,7 +82,7 @@ struct MigrateToAppGroupView: View {
case .migrating:
center {
ProgressView(value: 0.67)
Text("Migrating database archive")
Text("Migrating database archive...")
}
migrationProgress()
case .migration_error:

View File

@@ -1,9 +0,0 @@
//
// Keyboard.swift
// SimpleX (iOS)
//
// Created by Evgeny on 10/07/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import Foundation

View File

@@ -1,21 +0,0 @@
//
// KeyboardPadding.swift
// SimpleX (iOS)
//
// Created by Evgeny on 10/07/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder func keyboardPadding() -> some View {
if #available(iOS 17.0, *) {
GeometryReader { g in
self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom))
}
} else {
self
}
}
}

View File

@@ -36,7 +36,7 @@ struct AddGroupView: View {
}
}
} else {
createGroupView().keyboardPadding()
createGroupView()
}
}

View File

@@ -104,7 +104,6 @@ struct CreateProfile: View {
}
}
.padding()
.keyboardPadding()
}
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {

View File

@@ -18,7 +18,7 @@ struct TerminalView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State var composeState: ComposeState = ComposeState()
@State private var keyboardVisible = false
@FocusState private var keyboardVisible: Bool
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var terminalItem: TerminalItem?
@State private var scrolled = false

View File

@@ -53,7 +53,6 @@ struct AdvancedNetworkSettings: View {
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [2_500000, 5_000000, 7_500000, 10_000000, 15_000000, 20_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [1_500000, 3_000000, 5_000000, 7_000000, 10_000000, 15_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [5_000, 10_000, 20_000, 40_000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)

View File

@@ -587,10 +587,6 @@
<target>Povolit nevratné smazání odeslaných zpráv.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Povolit odesílání hlasových zpráv.</target>
@@ -2093,18 +2089,6 @@
<target>Soubory a média</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Konečně je máme! 🚀</target>
@@ -2215,10 +2199,6 @@
<target>Členové skupiny mohou posílat mizící zprávy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Členové skupiny mohou posílat hlasové zprávy.</target>
@@ -2947,10 +2927,6 @@
<target>Žádný token zařízení!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Skupina nebyla nalezena!</target>
@@ -3040,10 +3016,6 @@
<target>Předvolby skupiny mohou měnit pouze vlastníci skupiny.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</target>
@@ -3364,10 +3336,6 @@
<target>Zakázat posílání mizících zpráv.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Zakázat odesílání hlasových zpráv.</target>

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>Abbrechen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>Wechsel der Adresse abbrechen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>Wechsel der Adresse abbrechen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Das Senden von Sprachnachrichten erlauben.</target>
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>Fehler beim Abbrechen des Adresswechsels</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Favorit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>Dateien &amp; Medien</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Endlich haben wir sie! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>Gruppenmitglieder können verschwindende Nachrichten senden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Gruppenmitglieder können Sprachnachrichten versenden.</target>
@@ -2605,7 +2579,7 @@
</trans-unit>
<trans-unit id="KeyChain error" xml:space="preserve">
<source>KeyChain error</source>
<target>KeyChain Fehler</target>
<target>Keystore Fehler</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keychain error" xml:space="preserve">
@@ -2953,10 +2927,6 @@
<target>Kein Geräte-Token!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Die Gruppe wurde nicht gefunden!</target>
@@ -3046,10 +3016,6 @@
<target>Gruppenpräferenzen können nur von Gruppen-Eigentümern geändert werden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden.</target>
@@ -3370,10 +3336,6 @@
<target>Das Senden von verschwindenden Nachrichten verbieten.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Das Senden von Sprachnachrichten nicht erlauben.</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4480,7 +4441,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>Unfav.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -591,11 +591,6 @@
<target>Allow to irreversibly delete sent messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<target>Allow to send files and media.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Allow to send voice messages.</target>
@@ -2100,21 +2095,6 @@
<target>Files &amp; media</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<target>Files and media</target>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<target>Files and media are prohibited in this group.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<target>Files and media prohibited!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Finally, we have them! 🚀</target>
@@ -2225,11 +2205,6 @@
<target>Group members can send disappearing messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<target>Group members can send files and media.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Group members can send voice messages.</target>
@@ -2958,11 +2933,6 @@
<target>No device token!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<target>No filtered chats</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Group not found!</target>
@@ -3052,11 +3022,6 @@
<target>Only group owners can change group preferences.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<target>Only group owners can enable files and media.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Only group owners can enable voice messages.</target>
@@ -3377,11 +3342,6 @@
<target>Prohibit sending disappearing messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<target>Prohibit sending files and media.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Prohibit sending voice messages.</target>

View File

@@ -587,10 +587,6 @@
<target>Se permite la eliminación irreversible de mensajes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Permites enviar mensajes de voz.</target>
@@ -2093,18 +2089,6 @@
<target>Archivos y multimedia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>¡Por fin los tenemos! 🚀</target>
@@ -2215,10 +2199,6 @@
<target>Los miembros del grupo pueden enviar mensajes temporales.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Los miembros del grupo pueden enviar mensajes de voz.</target>
@@ -2947,10 +2927,6 @@
<target>¡Sin dispositivo token!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>¡Grupo no encontrado!</target>
@@ -3040,10 +3016,6 @@
<target>Sólo los propietarios pueden modificar las preferencias de grupo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Sólo los propietarios pueden activar los mensajes de voz.</target>
@@ -3364,10 +3336,6 @@
<target>No se permiten mensajes temporales.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>No se permiten mensajes de voz.</target>

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>Annuler</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>Annuler le changement d'adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>Abandonner le changement d'adresse ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>Le changement d'adresse sera annulé. L'ancienne adresse de réception sera utilisée.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>Autoriser la suppression irréversible de messages envoyés.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Autoriser l'envoi de messages vocaux.</target>
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>Erreur lors de l'annulation du changement d'adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Favoris</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>Fichiers &amp; médias</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Enfin, les voilà ! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>Les membres du groupes peuvent envoyer des messages éphémères.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Les membres du groupe peuvent envoyer des messages vocaux.</target>
@@ -2953,10 +2927,6 @@
<target>Pas de token d'appareil!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Groupe introuvable !</target>
@@ -3046,10 +3016,6 @@
<target>Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Seuls les propriétaires de groupes peuvent activer les messages vocaux.</target>
@@ -3370,10 +3336,6 @@
<target>Interdire lenvoi de messages éphémères.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Interdire l'envoi de messages vocaux.</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4480,7 +4441,6 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>Unfav.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -222,38 +222,32 @@ Available in v5.1</source>
<target state="translated">**הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target state="translated">**צור קישור / קוד QR** לשימוש איש הקשר שלך.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
<target state="translated">**יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
<target state="translated">**הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no">
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target state="translated">**שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
<target state="translated">**מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve" approved="no">
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target state="translated">**סרוק קוד QR**: כדי להתחבר לאיש הקשר שלך באופן אישי או באמצעות שיחת וידאו.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
@@ -268,52 +262,44 @@ Available in v5.1</source>
<source>**e2e encrypted** video call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="*bold*" xml:space="preserve" approved="no">
<trans-unit id="*bold*" xml:space="preserve">
<source>\*bold*</source>
<target state="translated">\*מובלט*</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=", " xml:space="preserve">
<source>, </source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="." xml:space="preserve" approved="no">
<trans-unit id="." xml:space="preserve">
<source>.</source>
<target state="needs-translation">.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="1 day" xml:space="preserve" approved="no">
<trans-unit id="1 day" xml:space="preserve">
<source>1 day</source>
<target state="translated">יום 1</target>
<note>message ttl</note>
</trans-unit>
<trans-unit id="1 hour" xml:space="preserve" approved="no">
<trans-unit id="1 hour" xml:space="preserve">
<source>1 hour</source>
<target state="translated">שעה</target>
<note>message ttl</note>
</trans-unit>
<trans-unit id="1 month" xml:space="preserve" approved="no">
<trans-unit id="1 month" xml:space="preserve">
<source>1 month</source>
<target state="translated">חודש</target>
<note>message ttl</note>
</trans-unit>
<trans-unit id="1 week" xml:space="preserve" approved="no">
<trans-unit id="1 week" xml:space="preserve">
<source>1 week</source>
<target state="translated">שבוע</target>
<note>message ttl</note>
</trans-unit>
<trans-unit id="2 weeks" xml:space="preserve">
<source>2 weeks</source>
<note>message ttl</note>
</trans-unit>
<trans-unit id="6" xml:space="preserve" approved="no">
<trans-unit id="6" xml:space="preserve">
<source>6</source>
<target state="translated">6</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=": " xml:space="preserve" approved="no">
<trans-unit id=": " xml:space="preserve">
<source>: </source>
<target state="translated">: </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A new contact" xml:space="preserve">
@@ -4239,26 +4225,6 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">%d שבועות</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1 minute" xml:space="preserve" approved="no">
<source>1 minute</source>
<target state="translated">דקה</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve" approved="no">
<source>1-time link</source>
<target state="translated">קישור חד־פעמי</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve" approved="no">
<source>5 minutes</source>
<target state="translated">5 דקות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="30 seconds" xml:space="preserve" approved="no">
<source>30 seconds</source>
<target state="translated">30 שניות</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="he" datatype="plaintext">

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>Interrompi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>Interrompi il cambio di indirizzo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>Interrompere il cambio di indirizzo?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>Permetti di eliminare irreversibilmente i messaggi inviati.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Permetti l'invio di messaggi vocali.</target>
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>Errore nell'interruzione del cambio di indirizzo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Preferito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>File e multimediali</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Finalmente le abbiamo! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>I membri del gruppo possono inviare messaggi a tempo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>I membri del gruppo possono inviare messaggi vocali.</target>
@@ -2953,10 +2927,6 @@
<target>Nessun token del dispositivo!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Gruppo non trovato!</target>
@@ -3046,10 +3016,6 @@
<target>Solo i proprietari del gruppo possono modificarne le preferenze.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Solo i proprietari del gruppo possono attivare i messaggi vocali.</target>
@@ -3370,10 +3336,6 @@
<target>Proibisci l'invio di messaggi a tempo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Proibisci l'invio di messaggi vocali.</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4480,7 +4441,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>Non pref.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -587,10 +587,6 @@
<target>送信済みメッセージの永久削除を許可する。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>音声メッセージの送信を許可する。</target>
@@ -2092,18 +2088,6 @@
<target>ファイルとメディア</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<note>No comment provided by engineer.</note>
@@ -2213,10 +2197,6 @@
<target>グループのメンバーが消えるメッセージを送信できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>グループのメンバーが音声メッセージを送信できます。</target>
@@ -2945,10 +2925,6 @@
<target>デバイストークンがありません!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>グループが見つかりません!</target>
@@ -3038,10 +3014,6 @@
<target>グループ設定を変えられるのはグループのオーナーだけです。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>音声メッセージを利用可能に設定できるのはグループのオーナーだけです。</target>
@@ -3362,10 +3334,6 @@
<target>消えるメッセージを使用禁止にする。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>音声メッセージを使用禁止にする。</target>

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>Afbreken</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>Annuleer het wijzigen van het adres</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>Adres wijziging afbreken?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>Sta toe om verzonden berichten onomkeerbaar te verwijderen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Sta toe om spraak berichten te verzenden.</target>
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>Fout bij het afbreken van adres wijziging</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Favoriet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>Bestanden en media</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Eindelijk, we hebben ze! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>Groepsleden kunnen verdwijnende berichten sturen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Groepsleden kunnen spraak berichten verzenden.</target>
@@ -2953,10 +2927,6 @@
<target>Geen apparaattoken!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Groep niet gevonden!</target>
@@ -3046,10 +3016,6 @@
<target>Alleen groep eigenaren kunnen groep voorkeuren wijzigen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Alleen groep eigenaren kunnen spraak berichten inschakelen.</target>
@@ -3370,10 +3336,6 @@
<target>Verbied het verzenden van verdwijnende berichten.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Verbieden het verzenden van spraak berichten.</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4480,7 +4441,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>Niet fav.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>Przerwij</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>Przerwij zmianę adresu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>Przerwać zmianę adresu?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>Zmiana adresu zostanie przerwana. Użyty zostanie stary adres odbiorczy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>Zezwól na nieodwracalne usunięcie wysłanych wiadomości.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Zezwól na wysyłanie wiadomości głosowych.</target>
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>Błąd przerwania zmiany adresu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Ulubione</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>Pliki i media</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>W końcu je mamy! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>Członkowie grupy mogą wysyłać znikające wiadomości.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Członkowie grupy mogą wysyłać wiadomości głosowe.</target>
@@ -2953,10 +2927,6 @@
<target>Brak tokenu urządzenia!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Nie znaleziono grupy!</target>
@@ -3046,10 +3016,6 @@
<target>Tylko właściciele grup mogą zmieniać preferencje grupy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Tylko właściciele grup mogą włączyć wiadomości głosowe.</target>
@@ -3370,10 +3336,6 @@
<target>Zabroń wysyłania znikających wiadomości.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Zabroń wysyłania wiadomości głosowych.</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4071,7 +4032,6 @@
</trans-unit>
<trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve">
<source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source>
<target>Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Somebody" xml:space="preserve">
@@ -4480,7 +4440,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>Nie ulub.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -587,10 +587,6 @@
<target>Разрешить необратимо удалять отправленные сообщения.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>Разрешить отправлять голосовые сообщения.</target>
@@ -2093,18 +2089,6 @@
<target>Файлы и медиа</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>Наконец-то, мы их добавили! 🚀</target>
@@ -2215,10 +2199,6 @@
<target>Члены группы могут посылать исчезающие сообщения.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>Члены группы могут отправлять голосовые сообщения.</target>
@@ -2947,10 +2927,6 @@
<target>Отсутствует токен устройства!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>Группа не найдена!</target>
@@ -3040,10 +3016,6 @@
<target>Только владельцы группы могут изменять предпочтения группы.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>Только владельцы группы могут разрешить голосовые сообщения.</target>
@@ -3364,10 +3336,6 @@
<target>Запретить посылать исчезающие сообщения.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>Запретить отправлять голосовые сообщений.</target>

View File

@@ -402,17 +402,14 @@
</trans-unit>
<trans-unit id="Abort" xml:space="preserve">
<source>Abort</source>
<target>中止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve">
<source>Abort changing address</source>
<target>中止地址更改</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve">
<source>Abort changing address?</source>
<target>中止地址更改?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve">
@@ -498,7 +495,6 @@
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target>将中止地址更改。将使用旧接收地址。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
@@ -591,10 +587,6 @@
<target>允许不可撤回地删除已发送消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target>允许发送语音消息。</target>
@@ -702,7 +694,7 @@
</trans-unit>
<trans-unit id="Audio and video calls" xml:space="preserve">
<source>Audio and video calls</source>
<target>语音和视频通话</target>
<target>视频通话</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
@@ -1806,7 +1798,6 @@
</trans-unit>
<trans-unit id="Error aborting address change" xml:space="preserve">
<source>Error aborting address change</source>
<target>中止地址更改错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error accepting contact request" xml:space="preserve">
@@ -2071,7 +2062,6 @@
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>最喜欢</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File will be deleted from servers." xml:space="preserve">
@@ -2099,18 +2089,6 @@
<target>文件和媒体</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media" xml:space="preserve">
<source>Files and media</source>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve">
<source>Files and media are prohibited in this group.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files and media prohibited!" xml:space="preserve">
<source>Files and media prohibited!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
<source>Finally, we have them! 🚀</source>
<target>终于我们有它们了! 🚀</target>
@@ -2221,10 +2199,6 @@
<target>群组成员可以发送限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send files and media." xml:space="preserve">
<source>Group members can send files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group members can send voice messages." xml:space="preserve">
<source>Group members can send voice messages.</source>
<target>群组成员可以发送语音消息。</target>
@@ -2600,7 +2574,7 @@
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source>
<target>加入群组</target>
<target>加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="KeyChain error" xml:space="preserve">
@@ -2953,10 +2927,6 @@
<target>无设备令牌!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
<source>No filtered chats</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No group!" xml:space="preserve">
<source>Group not found!</source>
<target>未找到群组!</target>
@@ -3046,10 +3016,6 @@
<target>只有群主可以改变群组偏好设置。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable files and media." xml:space="preserve">
<source>Only group owners can enable files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can enable voice messages." xml:space="preserve">
<source>Only group owners can enable voice messages.</source>
<target>只有群主可以启用语音信息。</target>
@@ -3370,10 +3336,6 @@
<target>禁止发送限时消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending files and media." xml:space="preserve">
<source>Prohibit sending files and media.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
<source>Prohibit sending voice messages.</source>
<target>禁止发送语音消息。</target>
@@ -3461,7 +3423,6 @@
</trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
<source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source>
<target>接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
@@ -4480,7 +4441,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unfav." xml:space="preserve">
<source>Unfav.</source>
<target>取消最喜欢</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unhide" xml:space="preserve">

View File

@@ -141,7 +141,6 @@
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
@@ -159,11 +158,6 @@
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
645041592A5C5749000221AD /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041542A5C5748000221AD /* libffi.a */; };
6450415A2A5C5749000221AD /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041552A5C5748000221AD /* libgmp.a */; };
6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */; };
6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */; };
6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041582A5C5748000221AD /* libgmpxx.a */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
@@ -171,6 +165,11 @@
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64A353102A4C84CE007CD71D /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530B2A4C84CE007CD71D /* libgmp.a */; };
64A353112A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */; };
64A353122A4C84CE007CD71D /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530D2A4C84CE007CD71D /* libffi.a */; };
64A353132A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */; };
64A353142A4C84CE007CD71D /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A3530F2A4C84CE007CD71D /* libgmpxx.a */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
@@ -418,7 +417,6 @@
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
@@ -435,11 +433,6 @@
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
645041542A5C5748000221AD /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
645041552A5C5748000221AD /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a"; sourceTree = "<group>"; };
645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a"; sourceTree = "<group>"; };
645041582A5C5748000221AD /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
@@ -448,6 +441,11 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64A3530B2A4C84CE007CD71D /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a"; sourceTree = "<group>"; };
64A3530D2A4C84CE007CD71D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a"; sourceTree = "<group>"; };
64A3530F2A4C84CE007CD71D /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
@@ -500,12 +498,12 @@
buildActionMask = 2147483647;
files = (
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
645041592A5C5749000221AD /* libffi.a in Frameworks */,
6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */,
6450415A2A5C5749000221AD /* libgmp.a in Frameworks */,
6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */,
64A353132A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a in Frameworks */,
64A353102A4C84CE007CD71D /* libgmp.a in Frameworks */,
64A353112A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a in Frameworks */,
64A353122A4C84CE007CD71D /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */,
64A353142A4C84CE007CD71D /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -566,11 +564,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
645041542A5C5748000221AD /* libffi.a */,
645041552A5C5748000221AD /* libgmp.a */,
645041582A5C5748000221AD /* libgmpxx.a */,
645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */,
645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */,
64A3530D2A4C84CE007CD71D /* libffi.a */,
64A3530B2A4C84CE007CD71D /* libgmp.a */,
64A3530F2A4C84CE007CD71D /* libgmpxx.a */,
64A3530E2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm-ghc8.10.7.a */,
64A3530C2A4C84CE007CD71D /* libHSsimplex-chat-5.2.0.0-ESKsZ4YorLH7yFQuFvHeIm.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -621,7 +619,6 @@
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
64466DCB29FFE3E800E3D48D /* MailView.swift */,
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -1145,7 +1142,6 @@
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
@@ -1474,7 +1470,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 153;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1516,7 +1512,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 153;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1596,7 +1592,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 153;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1628,7 +1624,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 153;
CURRENT_PROJECT_VERSION = 151;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;

View File

@@ -66,7 +66,6 @@ public enum ChatCommand {
case apiGetChatItemTTL(userId: Int64)
case apiSetNetworkConfig(networkConfig: NetCfg)
case apiGetNetworkConfig
case reconnectAllServers
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiContactInfo(contactId: Int64)
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
@@ -74,8 +73,6 @@ public enum ChatCommand {
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiAbortSwitchContact(contactId: Int64)
case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case apiSyncContactRatchet(contactId: Int64, force: Bool)
case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool)
case apiGetContactCode(contactId: Int64)
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
case apiVerifyContact(contactId: Int64, connectionCode: String?)
@@ -179,7 +176,6 @@ public enum ChatCommand {
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
case .apiGetNetworkConfig: return "/network"
case .reconnectAllServers: return "/reconnect"
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
@@ -187,16 +183,6 @@ public enum ChatCommand {
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)"
case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)"
case let .apiSyncContactRatchet(contactId, force): if force {
return "/_sync @\(contactId) force=on"
} else {
return "/_sync @\(contactId)"
}
case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force {
return "/_sync #\(groupId) \(groupMemberId) force=on"
} else {
return "/_sync #\(groupId) \(groupMemberId)"
}
case let .apiGetContactCode(contactId): return "/_get code @\(contactId)"
case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)"
case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)"
@@ -298,7 +284,6 @@ public enum ChatCommand {
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
case .reconnectAllServers: return "reconnectAllServers"
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiContactInfo: return "apiContactInfo"
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
@@ -306,8 +291,6 @@ public enum ChatCommand {
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
case .apiAbortSwitchContact: return "apiAbortSwitchContact"
case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember"
case .apiSyncContactRatchet: return "apiSyncContactRatchet"
case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet"
case .apiGetContactCode: return "apiGetContactCode"
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
case .apiVerifyContact: return "apiVerifyContact"
@@ -424,14 +407,6 @@ public enum ChatResponse: Decodable, Error {
case groupMemberSwitchStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitchAborted(user: User, contact: Contact, connectionStats: ConnectionStats)
case groupMemberSwitchAborted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitch(user: User, contact: Contact, switchProgress: SwitchProgress)
case groupMemberSwitch(user: User, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress)
case contactRatchetSyncStarted(user: User, contact: Contact, connectionStats: ConnectionStats)
case groupMemberRatchetSyncStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactRatchetSync(user: User, contact: Contact, ratchetSyncProgress: RatchetSyncProgress)
case groupMemberRatchetSync(user: User, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress)
case contactVerificationReset(user: User, contact: Contact)
case groupMemberVerificationReset(user: User, groupInfo: GroupInfo, member: GroupMember)
case contactCode(user: User, contact: Contact, connectionCode: String)
case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(user: User, verified: Bool, expectedCode: String)
@@ -555,14 +530,6 @@ public enum ChatResponse: Decodable, Error {
case .groupMemberSwitchStarted: return "groupMemberSwitchStarted"
case .contactSwitchAborted: return "contactSwitchAborted"
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted"
case .contactSwitch: return "contactSwitch"
case .groupMemberSwitch: return "groupMemberSwitch"
case .contactRatchetSyncStarted: return "contactRatchetSyncStarted"
case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted"
case .contactRatchetSync: return "contactRatchetSync"
case .groupMemberRatchetSync: return "groupMemberRatchetSync"
case .contactVerificationReset: return "contactVerificationReset"
case .groupMemberVerificationReset: return "groupMemberVerificationReset"
case .contactCode: return "contactCode"
case .groupMemberCode: return "groupMemberCode"
case .connectionVerified: return "connectionVerified"
@@ -685,14 +652,6 @@ public enum ChatResponse: Decodable, Error {
case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))")
case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))")
case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))")
case let .contactVerificationReset(u, contact): return withUser(u, "contact: \(String(describing: contact))")
case let .groupMemberVerificationReset(u, groupInfo, member): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))")
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
@@ -1035,7 +994,6 @@ public struct NetCfg: Codable, Equatable {
public var sessionMode: TransportSessionMode
public var tcpConnectTimeout: Int // microseconds
public var tcpTimeout: Int // microseconds
public var tcpTimeoutPerKb: Int // microseconds
public var tcpKeepAlive: KeepAliveOpts?
public var smpPingInterval: Int // microseconds
public var smpPingCount: Int // times
@@ -1046,7 +1004,6 @@ public struct NetCfg: Codable, Equatable {
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 10_000_000,
tcpTimeout: 7_000_000,
tcpTimeoutPerKb: 10_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1058,7 +1015,6 @@ public struct NetCfg: Codable, Equatable {
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 20_000_000,
tcpTimeout: 15_000_000,
tcpTimeoutPerKb: 20_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1144,20 +1100,9 @@ public struct ChatSettings: Codable {
public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, favorite: false)
}
public struct ConnectionStats: Decodable {
public var connAgentVersion: Int
public struct ConnectionStats: Codable {
public var rcvQueuesInfo: [RcvQueueInfo]
public var sndQueuesInfo: [SndQueueInfo]
public var ratchetSyncState: RatchetSyncState
public var ratchetSyncSupported: Bool
public var ratchetSyncAllowed: Bool {
ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState)
}
public var ratchetSyncSendProhibited: Bool {
[.required, .started, .agreed].contains(ratchetSyncState)
}
}
public struct RcvQueueInfo: Codable {
@@ -1183,30 +1128,6 @@ public enum SndSwitchStatus: String, Codable {
case sendingQTEST = "sending_qtest"
}
public enum QueueDirection: String, Decodable {
case rcv
case snd
}
public struct SwitchProgress: Decodable {
public var queueDirection: QueueDirection
public var switchPhase: SwitchPhase
public var connectionStats: ConnectionStats
}
public struct RatchetSyncProgress: Decodable {
public var ratchetSyncStatus: RatchetSyncState
public var connectionStats: ConnectionStats
}
public enum RatchetSyncState: String, Decodable {
case ok
case allowed
case required
case started
case agreed
}
public struct UserContactLink: Decodable {
public var connReqContact: String
public var autoAccept: AutoAccept?

View File

@@ -20,7 +20,6 @@ let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval"
let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount"
let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive"
@@ -43,7 +42,6 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval,
GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount,
GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive,
@@ -211,7 +209,6 @@ public func getNetCfg() -> NetCfg {
let sessionMode = networkSessionModeGroupDefault.get()
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE)
@@ -230,7 +227,6 @@ public func getNetCfg() -> NetCfg {
sessionMode: sessionMode,
tcpConnectTimeout: tcpConnectTimeout,
tcpTimeout: tcpTimeout,
tcpTimeoutPerKb: tcpTimeoutPerKb,
tcpKeepAlive: tcpKeepAlive,
smpPingInterval: smpPingInterval,
smpPingCount: smpPingCount,
@@ -243,7 +239,6 @@ public func setNetCfg(_ cfg: NetCfg) {
networkSessionModeGroupDefault.set(cfg.sessionMode)
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
if let tcpKeepAlive = cfg.tcpKeepAlive {

View File

@@ -1353,7 +1353,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var id: ChatId { get { "@\(contactId)" } }
public var apiId: Int64 { get { contactId } }
public var ready: Bool { get { activeConn.connStatus == .ready } }
public var sendMsgEnabled: Bool { get { !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false) } }
public var sendMsgEnabled: Bool { get { true } }
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
@@ -1426,12 +1426,6 @@ public struct Connection: Decodable {
public var customUserProfileId: Int64?
public var connectionCode: SecurityCode?
public var connectionStats: ConnectionStats? = nil
private enum CodingKeys: String, CodingKey {
case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
}
public var id: ChatId { get { ":\(connId)" } }
static let sampleData = Connection(
@@ -2462,15 +2456,11 @@ public enum CIContent: Decodable, ItemContent {
public enum MsgDecryptError: String, Decodable {
case ratchetHeader
case tooManySkipped
case ratchetEarlier
case other
var text: String {
switch self {
case .ratchetHeader: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
case .tooManySkipped: return NSLocalizedString("Permanent decryption error", comment: "message decrypt error item")
case .ratchetEarlier: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
case .other: return NSLocalizedString("Decryption error", comment: "message decrypt error item")
}
}
}
@@ -3067,8 +3057,6 @@ public enum SndGroupEvent: Decodable {
public enum RcvConnEvent: Decodable {
case switchQueue(phase: SwitchPhase)
case ratchetSync(syncStatus: RatchetSyncState)
case verificationCodeReset
var text: String {
switch self {
@@ -3076,51 +3064,25 @@ public enum RcvConnEvent: Decodable {
if case .completed = phase {
return NSLocalizedString("changed address for you", comment: "chat item text")
}
return NSLocalizedString("changing address", comment: "chat item text")
case let .ratchetSync(syncStatus):
return ratchetSyncStatusToText(syncStatus)
case .verificationCodeReset:
return NSLocalizedString("security code changed", comment: "chat item text")
return NSLocalizedString("changing address...", comment: "chat item text")
}
}
}
func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
switch ratchetSyncStatus {
case .ok: return NSLocalizedString("encryption ok", comment: "chat item text")
case .allowed: return NSLocalizedString("encryption re-negotiation allowed", comment: "chat item text")
case .required: return NSLocalizedString("encryption re-negotiation required", comment: "chat item text")
case .started: return NSLocalizedString("agreeing encryption…", comment: "chat item text")
case .agreed: return NSLocalizedString("encryption agreed", comment: "chat item text")
}
}
public enum SndConnEvent: Decodable {
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
var text: String {
switch self {
case let .switchQueue(phase, member):
if let name = member?.profile.profileViewName {
return phase == .completed
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@", comment: "chat item text"), name)
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
}
return phase == .completed
? NSLocalizedString("you changed address", comment: "chat item text")
: NSLocalizedString("changing address", comment: "chat item text")
case let .ratchetSync(syncStatus, member):
if let name = member?.profile.profileViewName {
switch syncStatus {
case .ok: return String.localizedStringWithFormat(NSLocalizedString("encryption ok for %@", comment: "chat item text"), name)
case .allowed: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation allowed for %@", comment: "chat item text"), name)
case .required: return String.localizedStringWithFormat(NSLocalizedString("encryption re-negotiation required for %@", comment: "chat item text"), name)
case .started: return String.localizedStringWithFormat(NSLocalizedString("agreeing encryption for %@…", comment: "chat item text"), name)
case .agreed: return String.localizedStringWithFormat(NSLocalizedString("encryption agreed for %@", comment: "chat item text"), name)
}
}
return ratchetSyncStatusToText(syncStatus)
? NSLocalizedString("you changed address", comment: "chat item text")
: NSLocalizedString("changing address...", comment: "chat item text")
}
}
}

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**Für jeden Kontakt und jedes Gruppenmitglied** wird eine separate TCP-Verbindung genutzt.\n**Bitte beachten Sie**: Wenn Sie viele Verbindungen haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern.";
/* No comment provided by engineer. */
"Abort" = "Abbrechen";
/* No comment provided by engineer. */
"Abort changing address" = "Wechsel der Adresse abbrechen";
/* No comment provided by engineer. */
"Abort changing address?" = "Wechsel der Adresse abbrechen?";
/* No comment provided by engineer. */
"About SimpleX" = "Über SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "Adresse";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.";
/* member role */
"admin" = "Admin";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "Fehler";
/* No comment provided by engineer. */
"Error aborting address change" = "Fehler beim Abbrechen des Adresswechsels";
/* No comment provided by engineer. */
"Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "Schnell und ohne warten auf den Absender, bis er online ist!";
/* No comment provided by engineer. */
"Favorite" = "Favorit";
/* No comment provided by engineer. */
"File will be deleted from servers." = "Die Datei wird von den Servern gelöscht.";
@@ -1756,7 +1738,7 @@
"Keychain error" = "Schlüsselbundfehler";
/* No comment provided by engineer. */
"KeyChain error" = "KeyChain Fehler";
"KeyChain error" = "Keystore Fehler";
/* No comment provided by engineer. */
"Large file!" = "Große Datei!";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "Empfangene Nachricht";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "Der Empfang der Datei wird beendet.";
@@ -2971,9 +2950,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "Unerwarteter Migrationsstatus";
/* No comment provided by engineer. */
"Unfav." = "Unfav.";
/* No comment provided by engineer. */
"Unhide" = "Verbergen aufheben";

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Une connexion TCP distincte sera utilisée **pour chaque contact et membre de groupe**.\n**Veuillez noter** : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.";
/* No comment provided by engineer. */
"Abort" = "Annuler";
/* No comment provided by engineer. */
"Abort changing address" = "Annuler le changement d'adresse";
/* No comment provided by engineer. */
"Abort changing address?" = "Abandonner le changement d'adresse ?";
/* No comment provided by engineer. */
"About SimpleX" = "À propos de SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "Adresse";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "Le changement d'adresse sera annulé. L'ancienne adresse de réception sera utilisée.";
/* member role */
"admin" = "admin";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "Erreur";
/* No comment provided by engineer. */
"Error aborting address change" = "Erreur lors de l'annulation du changement d'adresse";
/* No comment provided by engineer. */
"Error accepting contact request" = "Erreur de validation de la demande de contact";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "Rapide et ne nécessitant pas d'attendre que l'expéditeur soit en ligne !";
/* No comment provided by engineer. */
"Favorite" = "Favoris";
/* No comment provided by engineer. */
"File will be deleted from servers." = "Le fichier sera supprimé des serveurs.";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "Message reçu";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "La réception du fichier sera interrompue.";
@@ -2971,9 +2950,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "État de la migration inattendu";
/* No comment provided by engineer. */
"Unfav." = "Unfav.";
/* No comment provided by engineer. */
"Unhide" = "Dévoiler";

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Verrà usata una connessione TCP separata **per ogni contatto e membro del gruppo**.\n** Nota**: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.";
/* No comment provided by engineer. */
"Abort" = "Interrompi";
/* No comment provided by engineer. */
"Abort changing address" = "Interrompi il cambio di indirizzo";
/* No comment provided by engineer. */
"Abort changing address?" = "Interrompere il cambio di indirizzo?";
/* No comment provided by engineer. */
"About SimpleX" = "Riguardo SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "Indirizzo";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione.";
/* member role */
"admin" = "amministratore";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "Errore";
/* No comment provided by engineer. */
"Error aborting address change" = "Errore nell'interruzione del cambio di indirizzo";
/* No comment provided by engineer. */
"Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "Veloce e senza aspettare che il mittente sia in linea!";
/* No comment provided by engineer. */
"Favorite" = "Preferito";
/* No comment provided by engineer. */
"File will be deleted from servers." = "Il file verrà eliminato dai server.";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "Messaggio ricevuto";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "La ricezione del file verrà interrotta.";
@@ -2971,9 +2950,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "Stato di migrazione imprevisto";
/* No comment provided by engineer. */
"Unfav." = "Non pref.";
/* No comment provided by engineer. */
"Unhide" = "Svela";

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Er wordt een aparte TCP-verbinding gebruikt **voor elk contact en groepslid**.\n**Let op**: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen.";
/* No comment provided by engineer. */
"Abort" = "Afbreken";
/* No comment provided by engineer. */
"Abort changing address" = "Annuleer het wijzigen van het adres";
/* No comment provided by engineer. */
"Abort changing address?" = "Adres wijziging afbreken?";
/* No comment provided by engineer. */
"About SimpleX" = "Over SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "Adres";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt.";
/* member role */
"admin" = "Beheerder";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "Fout";
/* No comment provided by engineer. */
"Error aborting address change" = "Fout bij het afbreken van adres wijziging";
/* No comment provided by engineer. */
"Error accepting contact request" = "Fout bij het accepteren van een contactverzoek";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "Snel en niet wachten tot de afzender online is!";
/* No comment provided by engineer. */
"Favorite" = "Favoriet";
/* No comment provided by engineer. */
"File will be deleted from servers." = "Het bestand wordt van de servers verwijderd.";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "Ontvangen bericht";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "Het ontvangen van het bestand wordt gestopt.";
@@ -2971,9 +2950,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "Onverwachte migratiestatus";
/* No comment provided by engineer. */
"Unfav." = "Niet fav.";
/* No comment provided by engineer. */
"Unhide" = "zichtbaar maken";

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Oddzielne połączenie TCP będzie używane **dla każdego kontaktu i członka grupy**.\n**Uwaga**: jeśli masz wiele połączeń, zużycie baterii i ruchu może być znacznie wyższe, a niektóre połączenia mogą się nie udać.";
/* No comment provided by engineer. */
"Abort" = "Przerwij";
/* No comment provided by engineer. */
"Abort changing address" = "Przerwij zmianę adresu";
/* No comment provided by engineer. */
"Abort changing address?" = "Przerwać zmianę adresu?";
/* No comment provided by engineer. */
"About SimpleX" = "O SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "Adres";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "Zmiana adresu zostanie przerwana. Użyty zostanie stary adres odbiorczy.";
/* member role */
"admin" = "administrator";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "Błąd";
/* No comment provided by engineer. */
"Error aborting address change" = "Błąd przerwania zmiany adresu";
/* No comment provided by engineer. */
"Error accepting contact request" = "Błąd przyjmowania prośby o kontakt";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "Szybko i bez czekania aż nadawca będzie online!";
/* No comment provided by engineer. */
"Favorite" = "Ulubione";
/* No comment provided by engineer. */
"File will be deleted from servers." = "Plik zostanie usunięty z serwerów.";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "Otrzymano wiadomość";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "Odbieranie pliku zostanie przerwane.";
@@ -2719,9 +2698,6 @@
/* No comment provided by engineer. */
"SMP servers" = "Serwery SMP";
/* No comment provided by engineer. */
"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.";
/* notification title */
"Somebody" = "Ktoś";
@@ -2971,9 +2947,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "Nieoczekiwany stan migracji";
/* No comment provided by engineer. */
"Unfav." = "Nie ulub.";
/* No comment provided by engineer. */
"Unhide" = "Odkryj";

View File

@@ -247,15 +247,6 @@
/* No comment provided by engineer. */
"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "每个联系人和群成员将使用一个独立的 TCP 连接。\n请注意如果您有许多连接将会消耗更多的电量和流量并且某些连接可能会失败。";
/* No comment provided by engineer. */
"Abort" = "中止";
/* No comment provided by engineer. */
"Abort changing address" = "中止地址更改";
/* No comment provided by engineer. */
"Abort changing address?" = "中止地址更改?";
/* No comment provided by engineer. */
"About SimpleX" = "关于SimpleX";
@@ -311,9 +302,6 @@
/* No comment provided by engineer. */
"Address" = "地址";
/* No comment provided by engineer. */
"Address change will be aborted. Old receiving address will be used." = "将中止地址更改。将使用旧接收地址。";
/* member role */
"admin" = "管理员";
@@ -438,7 +426,7 @@
"Audio & video calls" = "语音和视频通话";
/* No comment provided by engineer. */
"Audio and video calls" = "语音和视频通话";
"Audio and video calls" = "视频通话";
/* No comment provided by engineer. */
"audio call (not e2e encrypted)" = "语音通话(非端到端加密)";
@@ -1230,9 +1218,6 @@
/* No comment provided by engineer. */
"Error" = "错误";
/* No comment provided by engineer. */
"Error aborting address change" = "中止地址更改错误";
/* No comment provided by engineer. */
"Error accepting contact request" = "接受联系人请求错误";
@@ -1389,9 +1374,6 @@
/* No comment provided by engineer. */
"Fast and no wait until the sender is online!" = "快速且无需等待发件人在线!";
/* No comment provided by engineer. */
"Favorite" = "最喜欢";
/* No comment provided by engineer. */
"File will be deleted from servers." = "文件将从服务器中删除。";
@@ -1750,7 +1732,7 @@
"Join incognito" = "加入隐身聊天";
/* No comment provided by engineer. */
"Joining group" = "加入群组";
"Joining group" = "加入群组";
/* No comment provided by engineer. */
"Keychain error" = "钥匙串错误";
@@ -2332,9 +2314,6 @@
/* message info title */
"Received message" = "收到的信息";
/* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。";
/* No comment provided by engineer. */
"Receiving file will be stopped." = "即将停止接收文件。";
@@ -2971,9 +2950,6 @@
/* No comment provided by engineer. */
"Unexpected migration state" = "未预料的迁移状态";
/* No comment provided by engineer. */
"Unfav." = "取消最喜欢";
/* No comment provided by engineer. */
"Unhide" = "取消隐藏";

View File

@@ -12,6 +12,7 @@ local.properties
common/src/commonMain/cpp/android/libs/
common/src/commonMain/cpp/desktop/libs/
common/src/commonMain/resources/libs/
android/src/main/cpp/libs/
android/build
android/release
common/build

View File

@@ -0,0 +1,243 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
compileSdk 33
defaultConfig {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 33
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode 129
versionName "5.2-beta.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild {
cmake {
cppFlags ''
}
}
manifestPlaceholders.app_name = "@string/app_name"
manifestPlaceholders.provider_authorities = "chat.simplex.app.provider"
manifestPlaceholders.extract_native_libs = compression_level != "0"
}
buildTypes {
debug {
applicationIdSuffix "$application_id_suffix"
debuggable new Boolean("$enable_debuggable")
manifestPlaceholders.app_name = "$app_name"
// Provider can't be the same for different apps on the same device
manifestPlaceholders.provider_authorities = "chat.simplex.app${application_id_suffix}.provider"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
jniLibs.useLegacyPackaging = compression_level != "0"
}
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs(
"en",
"cs",
"de",
"es",
"fr",
"it",
"ja",
"nl",
"pl",
"pt-rBR",
"ru",
"zh-rCN"
)
// }
if (isBundle) {
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
} else {
splits {
abi {
enable true
reset()
if (isRelease) {
include 'arm64-v8a', 'armeabi-v7a'
} else {
include 'arm64-v8a', 'armeabi-v7a'
universalApk false
}
}
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
implementation 'androidx.activity:activity-compose:1.4.0'
implementation 'androidx.fragment:fragment:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
implementation 'com.charleskorn.kaml:kaml:0.43.0'
//implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-util:$compose_version"
implementation "androidx.navigation:navigation-compose:2.4.1"
implementation "com.google.accompanist:accompanist-insets:0.23.0"
implementation 'androidx.webkit:webkit:1.4.0'
implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2"
def work_version = "2.7.1"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.work:work-multiprocess:$work_version"
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
//Barcode
implementation 'org.boofcv:boofcv-android:0.40.1'
implementation 'org.boofcv:boofcv-core:0.40.1'
//Camera Permission
implementation "com.google.accompanist:accompanist-permissions:0.23.0"
implementation "com.google.accompanist:accompanist-pager:0.25.1"
// Link Previews
implementation 'org.jsoup:jsoup:1.13.1'
// Biometric authentication
implementation 'androidx.biometric:biometric:1.2.0-alpha04'
// GIFs support
implementation "io.coil-kt:coil-compose:2.1.0"
implementation "io.coil-kt:coil-gif:2.1.0"
// Video support
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
// Wheel picker
implementation 'com.github.zj565061763:compose-wheel-picker:1.0.0-alpha10'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "com.jakewharton:process-phoenix:2.1.2"
}
// Don't do anything if no compression is needed
if (compression_level != "0") {
tasks.whenTaskAdded { task ->
if (task.name == 'packageDebug') {
task.finalizedBy compressApk
} else if (task.name == 'packageRelease') {
task.finalizedBy compressApk
}
}
}
tasks.register("compressApk") {
doLast {
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
def buildType
if (isRelease) {
buildType = "release"
} else {
buildType = "debug"
}
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
def sdkDir = android.getSdkDirectory().getAbsolutePath()
def keyAlias = ""
def keyPassword = ""
def storeFile = ""
def storePassword = ""
if (project.properties['android.injected.signing.key.alias'] != null) {
keyAlias = project.properties['android.injected.signing.key.alias']
keyPassword = project.properties['android.injected.signing.key.password']
storeFile = project.properties['android.injected.signing.store.file']
storePassword = project.properties['android.injected.signing.store.password']
} else if (android.signingConfigs.hasProperty(buildType)) {
def gradleConfig = android.signingConfigs[buildType]
keyAlias = gradleConfig.keyAlias
keyPassword = gradleConfig.keyPassword
storeFile = gradleConfig.storeFile
storePassword = gradleConfig.storePassword
} else {
// There is no signing config for current build type, can't sign the apk
println("No signing configs for this build type: $buildType")
return
}
def outputDir = tasks["package${buildType.capitalize()}"].outputs.files.last()
exec {
workingDir '../../../scripts/android'
setEnvironment(['JAVA_HOME': "$javaHome"])
commandLine './compress-and-sign-apk.sh', \
"$compression_level", \
"$outputDir", \
"$sdkDir", \
"$storeFile", \
"$storePassword", \
"$keyAlias", \
"$keyPassword"
}
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
new File(outputDir, "android-release.apk").renameTo(new File(outputDir, "simplex.apk"))
new File(outputDir, "android-armeabi-v7a-release.apk").renameTo(new File(outputDir, "simplex-armv7a.apk"))
new File(outputDir, "android-arm64-v8a-release.apk").renameTo(new File(outputDir, "simplex.apk"))
}
// View all gradle properties set
// project.properties.each { k, v -> println "$k -> $v" }
}
}

View File

@@ -1,210 +0,0 @@
@file:Suppress("UnstableApiUsage")
plugins {
id("com.android.application")
id("org.jetbrains.compose")
kotlin("android")
id("org.jetbrains.kotlin.plugin.serialization")
}
repositories {
maven("https://jitpack.io")
}
android {
compileSdkVersion(33)
defaultConfig {
applicationId = "chat.simplex.app"
minSdkVersion(26)
targetSdkVersion(33)
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode = (extra["android.version_code"] as String).toInt()
versionName = extra["android.version_name"] as String
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
externalNativeBuild {
cmake {
cppFlags("")
}
}
manifestPlaceholders["app_name"] = "@string/app_name"
manifestPlaceholders["provider_authorities"] = "chat.simplex.app.provider"
manifestPlaceholders["extract_native_libs"] = rootProject.extra["compression.level"] as Int != 0
}
buildTypes {
debug {
applicationIdSuffix = rootProject.extra["application_id.suffix"] as String
isDebuggable = rootProject.extra["enable_debuggable"] as Boolean
manifestPlaceholders["app_name"] = rootProject.extra["app.name"] as String
// Provider can"t be the same for different apps on the same device
manifestPlaceholders["provider_authorities"] = "chat.simplex.app${rootProject.extra["application_id.suffix"]}.provider"
}
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
externalNativeBuild {
cmake {
path(File("../common/src/commonMain/cpp/android/CMakeLists.txt"))
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
jniLibs.useLegacyPackaging = rootProject.extra["compression.level"] as Int != 0
}
val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null
val isBundle = gradle.startParameter.taskNames.find { it.toLowerCase().contains("bundle") } != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs(
"en",
"cs",
"de",
"es",
"fr",
"it",
"ja",
"nl",
"pl",
"pt-rBR",
"ru",
"zh-rCN"
)
// }
if (isBundle) {
defaultConfig.ndk.abiFilters("arm64-v8a", "armeabi-v7a")
} else {
splits {
abi {
isEnable = true
reset()
if (isRelease) {
include("arm64-v8a", "armeabi-v7a")
} else {
include("arm64-v8a", "armeabi-v7a")
isUniversalApk = false
}
}
}
}
}
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:1.7.0")
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$compose_version")
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.activity:activity-compose:1.5.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
implementation("com.google.accompanist:accompanist-pager:0.25.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose.version"] as String}")
}
tasks {
val compressApk by creating {
doLast {
val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null
val buildType: String = if (isRelease) "release" else "debug"
val javaHome = System.getProperties()["java.home"] ?: org.gradle.internal.jvm.Jvm.current().javaHome
val sdkDir = android.sdkDirectory.absolutePath
var keyAlias = ""
var keyPassword = ""
var storeFile = ""
var storePassword = ""
if (project.properties["android.injected.signing.key.alias"] != null) {
keyAlias = project.properties["android.injected.signing.key.alias"] as String
keyPassword = project.properties["android.injected.signing.key.password"] as String
storeFile = project.properties["android.injected.signing.store.file"] as String
storePassword = project.properties["android.injected.signing.store.password"] as String
} else {
try {
val gradleConfig = android.signingConfigs.getByName(buildType)
keyAlias = gradleConfig.keyAlias!!
keyPassword = gradleConfig.keyPassword!!
storeFile = gradleConfig.storeFile!!.absolutePath
storePassword = gradleConfig.storePassword!!
} catch (e: UnknownDomainObjectException) {
// There is no signing config for current build type, can"t sign the apk
println("No signing configs for this build type: $buildType")
return@doLast
}
}
lateinit var outputDir: File
named(if (isRelease) "packageRelease" else "packageDebug") {
outputDir = outputs.files.files.last()
}
exec {
workingDir("../../../scripts/android")
setEnvironment(mapOf("JAVA_HOME" to "$javaHome"))
commandLine = listOf(
"./compress-and-sign-apk.sh",
"${rootProject.extra["compression.level"]}",
"$outputDir",
"$sdkDir",
"$storeFile",
"$storePassword",
"$keyAlias",
"$keyPassword"
)
}
if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") {
File(outputDir, "android-release.apk").renameTo(File(outputDir, "simplex.apk"))
File(outputDir, "android-armeabi-v7a-release.apk").renameTo(File(outputDir, "simplex-armv7a.apk"))
File(outputDir, "android-arm64-v8a-release.apk").renameTo(File(outputDir, "simplex.apk"))
}
// View all gradle properties set
// project.properties.each { k, v -> println "$k -> $v" }
}
}
// Don"t do anything if no compression is needed
if (rootProject.extra["compression.level"] as Int != 0) {
whenTaskAdded {
if (name == "packageDebug") {
finalizedBy(compressApk)
} else if (name == "packageRelease") {
finalizedBy(compressApk)
}
}
}
}

View File

@@ -64,4 +64,4 @@ target_link_libraries( # Specifies the target library.
# Links the target library to the log library
# included in the NDK.
${log-lib})
${log-lib})

View File

@@ -17,17 +17,20 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.SplashView
import chat.simplex.app.views.call.ActiveCallView
import chat.simplex.app.views.call.IncomingCallAlertView
import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chat.group.ProgressIndicator
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
@@ -37,12 +40,8 @@ import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() {
companion object {
@@ -66,7 +65,6 @@ class MainActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SimplexApp.context.mainActivity = WeakReference(this)
// testJson()
val m = vm.chatModel
applyAppLocale(m.controller.appPrefs.appLanguage)
@@ -94,7 +92,7 @@ class MainActivity: FragmentActivity() {
destroyedAfterBackPress,
::runAuthenticate,
::setPerformLA,
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown) }
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
)
}
}
@@ -175,14 +173,15 @@ class MainActivity: FragmentActivity() {
withContext(Dispatchers.Main) {
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_unlock)
generalGetString(R.string.auth_unlock)
else
generalGetString(MR.strings.la_enter_app_passcode),
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_log_in_using_credential)
generalGetString(R.string.auth_log_in_using_credential)
else
generalGetString(MR.strings.auth_unlock),
generalGetString(R.string.auth_unlock),
selfDestruct = true,
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
@@ -208,49 +207,50 @@ class MainActivity: FragmentActivity() {
}
}
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
text = generalGetString(MR.strings.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(MR.strings.la_notice_turn_on),
title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = {
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
showChooseLAMode(laNoticeShown)
showChooseLAMode(laNoticeShown, activity)
}
}
)
}
}
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>) {
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
laNoticeShown.set(true)
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.la_lock_mode),
title = generalGetString(R.string.la_lock_mode),
text = null,
confirmText = generalGetString(MR.strings.la_lock_mode_passcode),
dismissText = generalGetString(MR.strings.la_lock_mode_system),
confirmText = generalGetString(R.string.la_lock_mode_passcode),
dismissText = generalGetString(R.string.la_lock_mode_system),
onConfirm = {
AlertManager.shared.hideAlert()
setPasscode()
},
onDismiss = {
AlertManager.shared.hideAlert()
initialEnableLA()
initialEnableLA(activity)
}
)
}
private fun initialEnableLA() {
private fun initialEnableLA(activity: FragmentActivity) {
val m = vm.chatModel
val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential),
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
activity = activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
@@ -297,26 +297,27 @@ class MainActivity: FragmentActivity() {
}
}
private fun setPerformLA(on: Boolean) {
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA()
enableLA(activity)
} else {
disableLA()
disableLA(activity)
}
}
private fun enableLA() {
private fun enableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_enable_simplex_lock)
generalGetString(R.string.auth_enable_simplex_lock)
else
generalGetString(MR.strings.new_passcode),
generalGetString(R.string.new_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_confirm_credential)
generalGetString(R.string.auth_confirm_credential)
else
"",
activity = activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
@@ -341,17 +342,18 @@ class MainActivity: FragmentActivity() {
)
}
private fun disableLA() {
private fun disableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_disable_simplex_lock)
generalGetString(R.string.auth_disable_simplex_lock)
else
generalGetString(MR.strings.la_enter_app_passcode),
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(MR.strings.auth_confirm_credential)
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(MR.strings.auth_disable_simplex_lock),
generalGetString(R.string.auth_disable_simplex_lock),
activity = activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
@@ -392,7 +394,7 @@ fun MainPage(
laFailed: MutableState<Boolean>,
destroyedAfterBackPress: MutableState<Boolean>,
runAuthenticate: () -> Unit,
setPerformLA: (Boolean) -> Unit,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
showLANotice: () -> Unit
) {
var showChatDatabaseError by rememberSaveable {
@@ -434,8 +436,8 @@ fun MainPage(
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(MR.strings.auth_unlock),
icon = painterResource(MR.images.ic_lock),
stringResource(R.string.auth_unlock),
icon = painterResource(R.drawable.ic_lock),
click = {
laFailed.value = false
runAuthenticate()
@@ -561,7 +563,7 @@ private fun InitializationView() {
color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp
)
Text(stringResource(MR.strings.opening_database))
Text(stringResource(R.string.opening_database))
}
}
}
@@ -602,7 +604,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.clearOverlays.value = true
val invitation = chatModel.callInvitations[chatId]
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
@@ -674,17 +676,17 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
} else {
withUriAction(uri) { linkType ->
val title = when (linkType) {
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link)
ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
}
AlertManager.shared.showAlertDialog(
title = title,
text = if (linkType == ConnectionLinkType.GROUP)
generalGetString(MR.strings.you_will_join_group)
generalGetString(R.string.you_will_join_group)
else
generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link),
confirmText = generalGetString(MR.strings.connect_via_link_verb),
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
confirmText = generalGetString(R.string.connect_via_link_verb),
onConfirm = {
withApi {
Log.d(TAG, "connectIfOpenedViaUri: connecting")

View File

@@ -14,7 +14,6 @@ import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
@@ -38,20 +37,13 @@ external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver {
var mainActivity: WeakReference<MainActivity> = WeakReference(null)
val chatModel: ChatModel
get() = chatController.chatModel
val appPreferences: AppPreferences
get() = chatController.appPrefs
val chatController: ChatController = ChatController
var isAppOnForeground: Boolean = false
val defaultLocale: Locale = Locale.getDefault()
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching {
@@ -84,14 +76,29 @@ class SimplexApp: Application(), LifecycleEventObserver {
chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
chatController.showBackgroundServiceNoticeIfNeeded()
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
SimplexService.start()
SimplexService.start(applicationContext)
}
}
}
}
val chatModel: ChatModel
get() = chatController.chatModel
private val ntfManager: NtfManager by lazy {
NtfManager(applicationContext, appPreferences)
}
private val appPreferences: AppPreferences by lazy {
AppPreferences(applicationContext)
}
val chatController: ChatController by lazy {
ChatController(0L, ntfManager, applicationContext, appPreferences)
}
override fun onCreate() {
super.onCreate()
@@ -134,7 +141,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
chatController.showBackgroundServiceNoticeIfNeeded()
}
/**
* We're starting service here instead of in [Lifecycle.Event.ON_START] because
@@ -145,7 +152,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
) {
SimplexService.start()
SimplexService.start(applicationContext)
}
}
else -> isAppOnForeground = false
@@ -155,12 +162,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
(!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
}
private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
}
/*

View File

@@ -1,28 +1,16 @@
package chat.simplex.app
import android.annotation.SuppressLint
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.net.Uri
import android.os.*
import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.ChatController
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationsMode
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// based on:
@@ -53,8 +41,8 @@ class SimplexService: Service() {
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Simplex service created")
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
val title = getString(R.string.simplex_service_notification_title)
val text = getString(R.string.simplex_service_notification_text)
notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text)
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
@@ -154,7 +142,7 @@ class SimplexService: Service() {
setupIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
setupIntent.putExtra(Settings.EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_ID)
val setup = PendingIntent.getActivity(this, 0, setupIntent, flags)
builder.addAction(0, generalGetString(MR.strings.hide_notification), setup)
builder.addAction(0, getString(R.string.hide_notification), setup)
}
return builder.build()
@@ -218,7 +206,7 @@ class SimplexService: Service() {
}
if (getServiceState(context) == ServiceState.STARTED) {
Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: $id)")
start()
start(context)
}
return Result.success()
}
@@ -259,7 +247,7 @@ class SimplexService: Service() {
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
}
suspend fun start() = serviceAction(Action.START)
suspend fun start(context: Context) = serviceAction(context, Action.START)
/**
* If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an
@@ -273,12 +261,12 @@ class SimplexService: Service() {
}
}
private suspend fun serviceAction(action: Action) {
private suspend fun serviceAction(context: Context, action: Action) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) {
Intent(SimplexApp.context, SimplexService::class.java).also {
Intent(context, SimplexService::class.java).also {
it.action = action.name
ContextCompat.startForegroundService(SimplexApp.context, it)
ContextCompat.startForegroundService(context, it)
}
}
}
@@ -307,15 +295,15 @@ class SimplexService: Service() {
}
val title = when(chatDbStatus) {
is DBMigrationResult.ErrorNotADatabase -> generalGetString(MR.strings.enter_passphrase_notification_title)
is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_title)
is DBMigrationResult.OK -> return
else -> generalGetString(MR.strings.database_initialization_error_title)
else -> generalGetString(R.string.database_initialization_error_title)
}
val description = when(chatDbStatus) {
is DBMigrationResult.ErrorNotADatabase -> generalGetString(MR.strings.enter_passphrase_notification_desc)
is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_desc)
is DBMigrationResult.OK -> return
else -> generalGetString(MR.strings.database_initialization_error_desc)
else -> generalGetString(R.string.database_initialization_error_desc)
}
val builder = NotificationCompat.Builder(SimplexApp.context, NOTIFICATION_CHANNEL_ID)
@@ -349,159 +337,5 @@ class SimplexService: Service() {
}
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
fun showBackgroundServiceNoticeIfNeeded() {
val appPrefs = ChatController.appPrefs
val mode = NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!)
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
// Nothing to do if mode is OFF. Can be selected on on-boarding stage
if (mode == NotificationsMode.OFF) return
if (!appPrefs.backgroundServiceNoticeShown.get()) {
// the branch for the new users who have never seen service notice
if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations()) {
showBGServiceNotice(mode)
} else {
showBGServiceNoticeIgnoreOptimization(mode)
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
appPrefs.backgroundServiceNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
} else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service
showDisablingServiceNotice(mode)
appPrefs.notificationsMode.set(NotificationsMode.OFF.name)
ChatModel.notificationsMode.value = NotificationsMode.OFF
SimplexService.StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll()
SimplexService.safeStopService(SimplexApp.context)
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization(mode)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
}
} else {
// service or periodic mode was chosen and battery optimization is disabled
SimplexApp.context.schedulePeriodicServiceRestartWorker()
SimplexApp.context.schedulePeriodicWakeUp()
}
}
private fun showBGServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(
annotatedStringResource(MR.strings.it_can_disabled_via_settings_notifications_still_shown)
)
}
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) }
}
)
}
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode) = AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
askAboutIgnoringBatteryOptimization()
}
AlertDialog(
onDismissRequest = ignoreOptimization,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
}
},
confirmButton = {
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.ok)) }
}
)
}
private fun showDisablingServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications_disabled) else stringResource(MR.strings.periodic_notifications_disabled),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
annotatedStringResource(MR.strings.turning_off_service_and_periodic),
Modifier.padding(bottom = 8.dp)
)
}
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) }
}
)
}
fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName)
}
private fun askAboutIgnoringBatteryOptimization() {
Intent().apply {
@SuppressLint("BatteryLife")
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${SimplexApp.context.packageName}")
// This flag is needed when you start a new activity from non-Activity context
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
SimplexApp.context.startActivity(this)
}
}
}
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.ComposeState
@@ -14,9 +15,6 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.app.views.usersettings.NotificationsMode
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
@@ -36,8 +34,7 @@ import kotlin.time.*
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
* */
@Stable
object ChatModel {
val controller: ChatController = ChatController
class ChatModel(val controller: ChatController) {
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
@@ -68,11 +65,11 @@ object ChatModel {
val appOpenUrl = mutableStateOf<Uri?>(null)
// preferences
val notificationsMode by lazy { mutableStateOf(NotificationsMode.values().firstOrNull { it.name == controller.appPrefs.notificationsMode.get() } ?: NotificationsMode.default) }
val notificationPreviewMode by lazy { mutableStateOf(NotificationPreviewMode.values().firstOrNull { it.name == controller.appPrefs.notificationPreviewMode.get() } ?: NotificationPreviewMode.default) }
val performLA by lazy { mutableStateOf(controller.appPrefs.performLA.get()) }
val notificationsMode = mutableStateOf(NotificationsMode.default)
var notificationPreviewMode = mutableStateOf(NotificationPreviewMode.default)
val performLA = mutableStateOf(false)
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
val incognito by lazy { mutableStateOf(controller.appPrefs.incognito.get()) }
var incognito = mutableStateOf(false)
// current WebRTC call
val callManager = CallManager(this)
@@ -94,7 +91,7 @@ object ChatModel {
val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>()
val simplexLinkMode by lazy { mutableStateOf(controller.appPrefs.simplexLinkMode.get()) }
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
@@ -130,36 +127,13 @@ object ChatModel {
fun updateChatInfo(cInfo: ChatInfo) {
val i = getChatIndex(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 newStats = newCInfo.contact.activeConn.connectionStats
if (currentStats != null && newStats == null) {
newCInfo = newCInfo.copy(
contact = newCInfo.contact.copy(
activeConn = newCInfo.contact.activeConn.copy(
connectionStats = currentStats
)
)
)
}
}
chats[i] = chats[i].copy(chatInfo = newCInfo)
}
if (i >= 0) chats[i] = chats[i].copy(chatInfo = cInfo)
}
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
val updatedConn = contact.activeConn.copy(connectionStats = connectionStats)
val updatedContact = contact.copy(activeConn = updatedConn)
updateContact(updatedContact)
}
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
private fun updateChat(cInfo: ChatInfo, addMissing: Boolean = true) {
@@ -458,15 +432,6 @@ object ChatModel {
}
}
fun updateGroupMemberConnectionStats(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(groupInfo, updatedMember)
}
}
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
}
@@ -727,7 +692,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val localAlias get() = ""
companion object {
private val invalidChatName = generalGetString(MR.strings.invalid_chat)
private val invalidChatName = generalGetString(R.string.invalid_chat)
}
}
@@ -743,15 +708,15 @@ sealed class ChatInfo: SomeChat, NamedChat {
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(MR.strings.server_connected)
is Error -> generalGetString(MR.strings.server_error)
else -> generalGetString(MR.strings.server_connecting)
is Connected -> generalGetString(R.string.server_connected)
is Error -> generalGetString(R.string.server_error)
else -> generalGetString(R.string.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@@ -778,7 +743,7 @@ data class Contact(
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready
override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)
override val sendMsgEnabled get() = true
override val ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
@@ -858,8 +823,7 @@ data class Connection(
val connLevel: Int,
val viaGroupLink: Boolean,
val customUserProfileId: Long? = null,
val connectionCode: SecurityCode? = null,
val connectionStats: ConnectionStats? = null
val connectionCode: SecurityCode? = null
) {
val id: ChatId get() = ":$connId"
companion object {
@@ -1097,10 +1061,10 @@ enum class GroupMemberRole(val memberRole: String) {
@SerialName("owner") Owner("owner");
val text: String get() = when (this) {
Observer -> generalGetString(MR.strings.group_member_role_observer)
Member -> generalGetString(MR.strings.group_member_role_member)
Admin -> generalGetString(MR.strings.group_member_role_admin)
Owner -> generalGetString(MR.strings.group_member_role_owner)
Observer -> generalGetString(R.string.group_member_role_observer)
Member -> generalGetString(R.string.group_member_role_member)
Admin -> generalGetString(R.string.group_member_role_admin)
Owner -> generalGetString(R.string.group_member_role_owner)
}
}
@@ -1128,31 +1092,31 @@ enum class GroupMemberStatus {
@SerialName("creator") MemCreator;
val text: String get() = when (this) {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left)
MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted)
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation)
MemAccepted -> generalGetString(MR.strings.group_member_status_accepted)
MemAnnounced -> generalGetString(MR.strings.group_member_status_announced)
MemConnected -> generalGetString(MR.strings.group_member_status_connected)
MemComplete -> generalGetString(MR.strings.group_member_status_complete)
MemCreator -> generalGetString(MR.strings.group_member_status_creator)
MemRemoved -> generalGetString(R.string.group_member_status_removed)
MemLeft -> generalGetString(R.string.group_member_status_left)
MemGroupDeleted -> generalGetString(R.string.group_member_status_group_deleted)
MemInvited -> generalGetString(R.string.group_member_status_invited)
MemIntroduced -> generalGetString(R.string.group_member_status_introduced)
MemIntroInvited -> generalGetString(R.string.group_member_status_intro_invitation)
MemAccepted -> generalGetString(R.string.group_member_status_accepted)
MemAnnounced -> generalGetString(R.string.group_member_status_announced)
MemConnected -> generalGetString(R.string.group_member_status_connected)
MemComplete -> generalGetString(R.string.group_member_status_complete)
MemCreator -> generalGetString(R.string.group_member_status_creator)
}
val shortText: String get() = when (this) {
MemRemoved -> generalGetString(MR.strings.group_member_status_removed)
MemLeft -> generalGetString(MR.strings.group_member_status_left)
MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted)
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting)
MemAccepted -> generalGetString(MR.strings.group_member_status_connecting)
MemAnnounced -> generalGetString(MR.strings.group_member_status_connecting)
MemConnected -> generalGetString(MR.strings.group_member_status_connected)
MemComplete -> generalGetString(MR.strings.group_member_status_complete)
MemCreator -> generalGetString(MR.strings.group_member_status_creator)
MemRemoved -> generalGetString(R.string.group_member_status_removed)
MemLeft -> generalGetString(R.string.group_member_status_left)
MemGroupDeleted -> generalGetString(R.string.group_member_status_group_deleted)
MemInvited -> generalGetString(R.string.group_member_status_invited)
MemIntroduced -> generalGetString(R.string.group_member_status_connecting)
MemIntroInvited -> generalGetString(R.string.group_member_status_connecting)
MemAccepted -> generalGetString(R.string.group_member_status_connecting)
MemAnnounced -> generalGetString(R.string.group_member_status_connecting)
MemConnected -> generalGetString(R.string.group_member_status_connected)
MemComplete -> generalGetString(R.string.group_member_status_complete)
MemCreator -> generalGetString(R.string.group_member_status_creator)
}
}
@@ -1241,17 +1205,17 @@ class PendingContactConnection(
override val incognito get() = customUserProfileId != null
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
override val localDisplayName get() = String.format(generalGetString(MR.strings.connection_local_display_name), pccConnId)
override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId)
override val displayName: String get() {
if (localAlias.isNotEmpty()) return localAlias
val initiated = pccConnStatus.initiated
return if (initiated == null) {
// this should not be in the chat list
generalGetString(MR.strings.display_name_connection_established)
generalGetString(R.string.display_name_connection_established)
} else {
generalGetString(
if (initiated && !viaContactUri) MR.strings.display_name_invited_to_connect
else MR.strings.display_name_connecting
if (initiated && !viaContactUri) R.string.display_name_invited_to_connect
else R.string.display_name_connecting
)
}
}
@@ -1264,14 +1228,14 @@ class PendingContactConnection(
val initiated = pccConnStatus.initiated
return if (initiated == null) "" else generalGetString(
if (initiated && !viaContactUri)
if (incognito) MR.strings.description_you_shared_one_time_link_incognito else MR.strings.description_you_shared_one_time_link
if (incognito) R.string.description_you_shared_one_time_link_incognito else R.string.description_you_shared_one_time_link
else if (viaContactUri)
if (groupLinkId != null)
if (incognito) MR.strings.description_via_group_link_incognito else MR.strings.description_via_group_link
if (incognito) R.string.description_via_group_link_incognito else R.string.description_via_group_link
else
if (incognito) MR.strings.description_via_contact_address_link_incognito else MR.strings.description_via_contact_address_link
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
else
if (incognito) MR.strings.description_via_one_time_link_incognito else MR.strings.description_via_one_time_link
if (incognito) R.string.description_via_one_time_link_incognito else R.string.description_via_one_time_link
)
}
@@ -1347,7 +1311,7 @@ data class ChatItem (
val text: String get() {
val mc = content.msgContent
return when {
content.text == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(MR.strings.voice_message_with_duration), durationText(mc.duration))
content.text == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(R.string.voice_message_with_duration), durationText(mc.duration))
content.text == "" && file != null -> file.fileName
else -> content.text
}
@@ -1526,7 +1490,7 @@ data class ChatItem (
meta = CIMeta(
itemId = TEMP_DELETED_CHAT_ITEM_ID,
itemTs = Clock.System.now(),
itemText = generalGetString(MR.strings.deleted_description),
itemText = generalGetString(R.string.deleted_description),
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
@@ -1611,12 +1575,12 @@ data class CIMeta (
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<ImageResource, Color>? =
fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<Int, Color>? =
when (itemStatus) {
is CIStatus.SndSent -> MR.images.ic_check_filled to metaColor
is CIStatus.SndErrorAuth -> MR.images.ic_close to Color.Red
is CIStatus.SndError -> MR.images.ic_warning_filled to WarningYellow
is CIStatus.RcvNew -> MR.images.ic_circle_filled to primaryColor
is CIStatus.SndSent -> R.drawable.ic_check_filled to metaColor
is CIStatus.SndErrorAuth -> R.drawable.ic_close to Color.Red
is CIStatus.SndError -> R.drawable.ic_warning_filled to WarningYellow
is CIStatus.RcvNew -> R.drawable.ic_circle_filled to primaryColor
else -> null
}
@@ -1753,8 +1717,8 @@ sealed class CIContent: ItemContent {
override val text: String get() = when (this) {
is SndMsgContent -> msgContent.text
is RcvMsgContent -> msgContent.text
is SndDeleted -> generalGetString(MR.strings.deleted_description)
is RcvDeleted -> generalGetString(MR.strings.deleted_description)
is SndDeleted -> generalGetString(R.string.deleted_description)
is RcvDeleted -> generalGetString(R.string.deleted_description)
is SndCall -> status.text(duration)
is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text
@@ -1771,10 +1735,10 @@ sealed class CIContent: ItemContent {
is SndChatPreference -> preferenceText(feature, allowed, param)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is SndModerated -> generalGetString(MR.strings.moderated_description)
is RcvModerated -> generalGetString(MR.strings.moderated_description)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is SndModerated -> generalGetString(R.string.moderated_description)
is RcvModerated -> generalGetString(R.string.moderated_description)
is InvalidJSON -> "invalid data"
}
@@ -1788,11 +1752,11 @@ sealed class CIContent: ItemContent {
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
String.format(generalGetString(MR.strings.feature_offered_item_with_param), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, timeText(param))
allowed != FeatureAllowed.NO ->
String.format(generalGetString(MR.strings.feature_offered_item), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_offered_item), feature.text, timeText(param))
else ->
String.format(generalGetString(MR.strings.feature_cancelled_item), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, timeText(param))
}
}
}
@@ -1800,15 +1764,11 @@ sealed class CIContent: ItemContent {
@Serializable
enum class MsgDecryptError {
@SerialName("ratchetHeader") RatchetHeader,
@SerialName("tooManySkipped") TooManySkipped,
@SerialName("ratchetEarlier") RatchetEarlier,
@SerialName("other") Other;
@SerialName("tooManySkipped") TooManySkipped;
val text: String get() = when (this) {
RatchetHeader -> generalGetString(MR.strings.decryption_error)
TooManySkipped -> generalGetString(MR.strings.decryption_error)
RatchetEarlier -> generalGetString(MR.strings.decryption_error)
Other -> generalGetString(MR.strings.decryption_error)
RatchetHeader -> generalGetString(R.string.decryption_error)
TooManySkipped -> generalGetString(R.string.decryption_error)
}
}
@@ -1830,7 +1790,7 @@ class CIQuote (
fun sender(membership: GroupMember?): String? = when (chatDir) {
is CIDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.DirectSnd -> generalGetString(R.string.sender_you_pronoun)
is CIDirection.DirectRcv -> null
is CIDirection.GroupSnd -> membership?.displayName
is CIDirection.GroupRcv -> chatDir.groupMember.displayName
@@ -1935,7 +1895,6 @@ class CIFile(
is CIFileStatus.RcvError -> false
}
@Transient
val cancelAction: CancelAction? = when (fileStatus) {
is CIFileStatus.SndStored -> sndCancelAction
is CIFileStatus.SndTransfer -> sndCancelAction
@@ -1967,39 +1926,41 @@ class CIFile(
}
}
@Serializable
class CancelAction(
val uiActionId: StringResource,
val uiActionId: Int,
val alert: AlertInfo
)
@Serializable
class AlertInfo(
val titleId: StringResource,
val messageId: StringResource,
val confirmId: StringResource
val titleId: Int,
val messageId: Int,
val confirmId: Int
)
private val sndCancelAction: CancelAction = CancelAction(
uiActionId = MR.strings.stop_file__action,
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = MR.strings.stop_snd_file__title,
messageId = MR.strings.stop_snd_file__message,
confirmId = MR.strings.stop_file__confirm
titleId = R.string.stop_snd_file__title,
messageId = R.string.stop_snd_file__message,
confirmId = R.string.stop_file__confirm
)
)
private val revokeCancelAction: CancelAction = CancelAction(
uiActionId = MR.strings.revoke_file__action,
uiActionId = R.string.revoke_file__action,
alert = AlertInfo(
titleId = MR.strings.revoke_file__title,
messageId = MR.strings.revoke_file__message,
confirmId = MR.strings.revoke_file__confirm
titleId = R.string.revoke_file__title,
messageId = R.string.revoke_file__message,
confirmId = R.string.revoke_file__confirm
)
)
private val rcvCancelAction: CancelAction = CancelAction(
uiActionId = MR.strings.stop_file__action,
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = MR.strings.stop_rcv_file__title,
messageId = MR.strings.stop_rcv_file__message,
confirmId = MR.strings.stop_file__confirm
titleId = R.string.stop_rcv_file__title,
messageId = R.string.stop_rcv_file__message,
confirmId = R.string.stop_file__confirm
)
)
@@ -2050,7 +2011,7 @@ class CIGroupInvitation (
val status: CIGroupInvitationStatus,
) {
val text: String get() = String.format(
generalGetString(MR.strings.group_invitation_item_description),
generalGetString(R.string.group_invitation_item_description),
groupProfile.displayName)
companion object {
@@ -2103,7 +2064,7 @@ object MsgContentSerializer : KSerializer<MsgContent> {
return if (json is JsonObject) {
if ("type" in json) {
val t = json["type"]?.jsonPrimitive?.content ?: ""
val text = json["text"]?.jsonPrimitive?.content ?: generalGetString(MR.strings.unknown_message_format)
val text = json["text"]?.jsonPrimitive?.content ?: generalGetString(R.string.unknown_message_format)
when (t) {
"text" -> MsgContent.MCText(text)
"link" -> {
@@ -2127,10 +2088,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
else -> MsgContent.MCUnknown(t, text, json)
}
} else {
MsgContent.MCUnknown(text = generalGetString(MR.strings.invalid_message_format), json = json)
MsgContent.MCUnknown(text = generalGetString(R.string.invalid_message_format), json = json)
}
} else {
MsgContent.MCUnknown(text = generalGetString(MR.strings.invalid_message_format), json = json)
MsgContent.MCUnknown(text = generalGetString(R.string.invalid_message_format), json = json)
}
}
@@ -2194,7 +2155,7 @@ class FormattedText(val text: String, val format: Format? = null) {
if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text
fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List<String>): String =
"${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})"
"${linkType.description} (${String.format(generalGetString(R.string.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})"
}
@Serializable
@@ -2235,9 +2196,9 @@ enum class SimplexLinkType(val linkType: String) {
group("group");
val description: String get() = generalGetString(when (this) {
contact -> MR.strings.simplex_link_contact
invitation -> MR.strings.simplex_link_invitation
group -> MR.strings.simplex_link_group
contact -> R.string.simplex_link_contact
invitation -> R.string.simplex_link_invitation
group -> R.string.simplex_link_group
})
}
@@ -2285,14 +2246,14 @@ enum class CICallStatus {
@SerialName("error") Error;
fun text(sec: Int): String = when (this) {
Pending -> generalGetString(MR.strings.callstatus_calling)
Missed -> generalGetString(MR.strings.callstatus_missed)
Rejected -> generalGetString(MR.strings.callstatus_rejected)
Accepted -> generalGetString(MR.strings.callstatus_accepted)
Negotiated -> generalGetString(MR.strings.callstatus_connecting)
Progress -> generalGetString(MR.strings.callstatus_in_progress)
Ended -> String.format(generalGetString(MR.strings.callstatus_ended), durationText(sec))
Error -> generalGetString(MR.strings.callstatus_error)
Pending -> generalGetString(R.string.callstatus_calling)
Missed -> generalGetString(R.string.callstatus_missed)
Rejected -> generalGetString(R.string.callstatus_rejected)
Accepted -> generalGetString(R.string.callstatus_accepted)
Negotiated -> generalGetString(R.string.callstatus_connecting)
Progress -> generalGetString(R.string.callstatus_in_progress)
Ended -> String.format(generalGetString(R.string.callstatus_ended), durationText(sec))
Error -> generalGetString(R.string.callstatus_error)
}
}
@@ -2310,10 +2271,10 @@ sealed class MsgErrorType() {
@Serializable @SerialName("msgDuplicate") class MsgDuplicate(): MsgErrorType()
val text: String get() = when (this) {
is MsgSkipped -> String.format(generalGetString(MR.strings.integrity_msg_skipped), toMsgId - fromMsgId + 1)
is MsgBadHash -> generalGetString(MR.strings.integrity_msg_bad_hash) // not used now
is MsgBadId -> generalGetString(MR.strings.integrity_msg_bad_id) // not used now
is MsgDuplicate -> generalGetString(MR.strings.integrity_msg_duplicate) // not used now
is MsgSkipped -> String.format(generalGetString(R.string.integrity_msg_skipped), toMsgId - fromMsgId + 1)
is MsgBadHash -> generalGetString(R.string.integrity_msg_bad_hash) // not used now
is MsgBadId -> generalGetString(R.string.integrity_msg_bad_id) // not used now
is MsgDuplicate -> generalGetString(R.string.integrity_msg_duplicate) // not used now
}
}
@@ -2331,16 +2292,16 @@ sealed class RcvGroupEvent() {
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
val text: String get() = when (this) {
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected)
is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left)
is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text)
is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName)
is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted)
is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted)
is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile)
is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.profileViewName)
is MemberConnected -> generalGetString(R.string.rcv_group_event_member_connected)
is MemberLeft -> generalGetString(R.string.rcv_group_event_member_left)
is MemberRole -> String.format(generalGetString(R.string.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.rcv_group_event_changed_your_role), role.text)
is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.profileViewName)
is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted)
is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted)
is GroupUpdated -> generalGetString(R.string.rcv_group_event_updated_group_profile)
is InvitedViaGroupLink -> generalGetString(R.string.rcv_group_event_invited_via_your_group_link)
}
}
@@ -2353,72 +2314,44 @@ sealed class SndGroupEvent() {
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent()
val text: String get() = when (this) {
is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text)
is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName)
is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left)
is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated)
is MemberRole -> String.format(generalGetString(R.string.snd_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.snd_group_event_changed_role_for_yourself), role.text)
is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.profileViewName)
is UserLeft -> generalGetString(R.string.snd_group_event_user_left)
is GroupUpdated -> generalGetString(R.string.snd_group_event_group_profile_updated)
}
}
@Serializable
sealed class RcvConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent()
@Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent()
val text: String get() = when (this) {
is SwitchQueue -> when (phase) {
SwitchPhase.Completed -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_completed)
else -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_changing)
SwitchPhase.Completed -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_completed)
else -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_changing)
}
is RatchetSync -> ratchetSyncStatusToText(syncStatus)
is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset)
}
}
fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String {
return when (ratchetSyncStatus) {
RatchetSyncState.Ok -> generalGetString(MR.strings.conn_event_ratchet_sync_ok)
RatchetSyncState.Allowed -> generalGetString(MR.strings.conn_event_ratchet_sync_allowed)
RatchetSyncState.Required -> generalGetString(MR.strings.conn_event_ratchet_sync_required)
RatchetSyncState.Started -> generalGetString(MR.strings.conn_event_ratchet_sync_started)
RatchetSyncState.Agreed -> generalGetString(MR.strings.conn_event_ratchet_sync_agreed)
}
}
@Serializable
sealed class SndConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent()
val text: String
get() = when (this) {
is SwitchQueue -> {
member?.profile?.profileViewName?.let {
return when (phase) {
SwitchPhase.Completed -> String.format(generalGetString(MR.strings.snd_conn_event_switch_queue_phase_completed_for_member), it)
else -> String.format(generalGetString(MR.strings.snd_conn_event_switch_queue_phase_changing_for_member), it)
SwitchPhase.Completed -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_completed_for_member), it)
else -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_changing_for_member), it)
}
}
when (phase) {
SwitchPhase.Completed -> generalGetString(MR.strings.snd_conn_event_switch_queue_phase_completed)
else -> generalGetString(MR.strings.snd_conn_event_switch_queue_phase_changing)
SwitchPhase.Completed -> generalGetString(R.string.snd_conn_event_switch_queue_phase_completed)
else -> generalGetString(R.string.snd_conn_event_switch_queue_phase_changing)
}
}
is RatchetSync -> {
member?.profile?.profileViewName?.let {
return when (syncStatus) {
RatchetSyncState.Ok -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_ok), it)
RatchetSyncState.Allowed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_allowed), it)
RatchetSyncState.Required -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_required), it)
RatchetSyncState.Started -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_started), it)
RatchetSyncState.Agreed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_agreed), it)
}
}
ratchetSyncStatusToText(syncStatus)
}
}
}

View File

@@ -1,48 +1,45 @@
package chat.simplex.app.model
import android.Manifest
import android.app.*
import android.app.TaskStackBuilder
import android.content.*
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.hardware.display.DisplayManager
import android.media.AudioAttributes
import android.net.Uri
import android.util.Log
import android.view.Display
import androidx.core.app.*
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import chat.simplex.app.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chatlist.acceptContactRequest
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.res.MR
import kotlinx.datetime.Clock
object NtfManager {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
class NtfManager(val context: Context, private val appPreferences: AppPreferences) {
companion object {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT"
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId"
private val appPreferences: AppPreferences by lazy { ChatController.appPrefs }
private val context: Context
get() = SimplexApp.context
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId"
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
}
private val manager: NotificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private var prevNtfTime = mutableMapOf<String, Long>()
private val msgNtfTimeoutMs = 30000L
@@ -61,7 +58,7 @@ object NtfManager {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
Log.d(TAG, "callNotificationChannel sound: $soundUri")
Log.d(TAG,"callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true)
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
@@ -73,8 +70,8 @@ object NtfManager {
fun cancelNotificationsForChat(chatId: String) {
prevNtfTime.remove(chatId)
manager.cancel(chatId.hashCode())
val msgNtfs = manager.activeNotifications.filter { ntf ->
ntf.notification.channelId == MessageChannel
val msgNtfs = manager.activeNotifications.filter {
ntf -> ntf.notification.channelId == MessageChannel
}
if (msgNtfs.count() == 1) {
// Have a group notification with no children so cancel it
@@ -87,7 +84,7 @@ object NtfManager {
user = user,
chatId = cInfo.id,
displayName = cInfo.displayName,
msgText = generalGetString(MR.strings.notification_new_contact_request),
msgText = generalGetString(R.string.notification_new_contact_request),
image = cInfo.image,
listOf(NotificationAction.ACCEPT_CONTACT_REQUEST)
)
@@ -98,7 +95,7 @@ object NtfManager {
user = user,
chatId = contact.id,
displayName = contact.displayName,
msgText = generalGetString(MR.strings.notification_contact_connected)
msgText = generalGetString(R.string.notification_contact_connected)
)
}
@@ -113,9 +110,10 @@ object NtfManager {
val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
prevNtfTime[chatId] = now
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(R.string.notification_preview_somebody) else displayName
val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(R.string.notification_preview_new_message) else msgText
val largeIcon = when {
actions.isEmpty() -> null
image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon)
@@ -143,10 +141,11 @@ object NtfManager {
actionIntent.putExtra(ChatIdKey, chatId)
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
val actionButton = when (action) {
NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept)
NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(R.string.accept)
}
builder.addAction(0, actionButton, actionPendingIntent)
}
val summary = NotificationCompat.Builder(context, MessageChannel)
.setSmallIcon(R.drawable.ntf_icon)
.setColor(0x88FFFF)
@@ -158,17 +157,14 @@ object NtfManager {
with(NotificationManagerCompat.from(context)) {
// using cInfo.id only shows one notification per chat and updates it when the message arrives
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(chatId.hashCode(), builder.build())
notify(0, summary)
}
notify(chatId.hashCode(), builder.build())
notify(0, summary)
}
}
fun notifyCallInvitation(invitation: RcvCallInvitation) {
val keyguardManager = getKeyguardManager(context)
Log.d(
TAG,
Log.d(TAG,
"notifyCallInvitation pre-requests: " +
"keyguard locked ${keyguardManager.isKeyguardLocked}, " +
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
@@ -192,21 +188,21 @@ object NtfManager {
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel)
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
}
val text = generalGetString(
if (invitation.callType.media == CallMediaType.Video) {
if (invitation.sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call
if (invitation.sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
} else {
if (invitation.sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call
if (invitation.sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
}
)
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
generalGetString(MR.strings.notification_preview_somebody)
generalGetString(R.string.notification_preview_somebody)
else
invitation.contact.displayName
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
@@ -227,9 +223,7 @@ object NtfManager {
// This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notify(CallNotificationId, notification)
}
notify(CallNotificationId, notification)
}
}
@@ -243,7 +237,7 @@ object NtfManager {
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem): String {
private fun hideSecrets(cItem: ChatItem) : String {
val md = cItem.formattedText
return if (md != null) {
var res = ""
@@ -282,8 +276,8 @@ object NtfManager {
* old ones if needed
* */
fun createNtfChannelsMaybeShowAlert() {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(MR.strings.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls)))
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
@@ -308,16 +302,14 @@ object NtfManager {
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
cancelNotificationsForChat(chatId)
m.controller.ntfManager.cancelNotificationsForChat(chatId)
}
RejectCallAction -> {
val invitation = m.callInvitations[chatId]
if (invitation != null) {
m.callManager.endCall(invitation = invitation)
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
}

View File

@@ -13,10 +13,10 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.internal.toHexString
enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX;
@@ -70,15 +70,15 @@ enum class ThemeColor {
val text: String
get() = when (this) {
PRIMARY -> generalGetString(MR.strings.color_primary)
PRIMARY_VARIANT -> generalGetString(MR.strings.color_primary_variant)
SECONDARY -> generalGetString(MR.strings.color_secondary)
SECONDARY_VARIANT -> generalGetString(MR.strings.color_secondary_variant)
BACKGROUND -> generalGetString(MR.strings.color_background)
SURFACE -> generalGetString(MR.strings.color_surface)
TITLE -> generalGetString(MR.strings.color_title)
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
PRIMARY -> generalGetString(R.string.color_primary)
PRIMARY_VARIANT -> generalGetString(R.string.color_primary_variant)
SECONDARY -> generalGetString(R.string.color_secondary)
SECONDARY_VARIANT -> generalGetString(R.string.color_secondary_variant)
BACKGROUND -> generalGetString(R.string.color_background)
SURFACE -> generalGetString(R.string.color_surface)
TITLE -> generalGetString(R.string.color_title)
SENT_MESSAGE -> generalGetString(R.string.color_sent_message)
RECEIVED_MESSAGE -> generalGetString(R.string.color_received_message)
}
}
@@ -150,7 +150,7 @@ data class ThemeColors(
private fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()
@Serializable
data class ThemeOverrides (

View File

@@ -7,7 +7,7 @@ import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import okhttp3.internal.toHexString
object ThemeManager {
private val appPrefs: AppPreferences by lazy {
@@ -63,28 +63,28 @@ object ThemeManager {
Triple(
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
DefaultTheme.SYSTEM,
generalGetString(MR.strings.theme_system)
generalGetString(R.string.theme_system)
)
)
allThemes.add(
Triple(
LightColorPalette,
DefaultTheme.LIGHT,
generalGetString(MR.strings.theme_light)
generalGetString(R.string.theme_light)
)
)
allThemes.add(
Triple(
DarkColorPalette,
DefaultTheme.DARK,
generalGetString(MR.strings.theme_dark)
generalGetString(R.string.theme_dark)
)
)
allThemes.add(
Triple(
SimplexColorPalette,
DefaultTheme.SIMPLEX,
generalGetString(MR.strings.theme_simplex)
generalGetString(R.string.theme_simplex)
)
)
return allThemes
@@ -149,4 +149,4 @@ object ThemeManager {
}
}
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()

View File

@@ -4,16 +4,16 @@ import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.unit.sp
import chat.simplex.res.MR
import chat.simplex.app.R
// https://github.com/rsms/inter
val Inter: FontFamily = FontFamily(
Font(MR.fonts.Inter.regular.fontResourceId),
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic),
Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold),
Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold),
Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium),
Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light)
val Inter = FontFamily(
Font(R.font.inter_regular),
Font(R.font.inter_italic, style = FontStyle.Italic),
Font(R.font.inter_bold, weight = FontWeight.Bold),
Font(R.font.inter_semi_bold, weight = FontWeight.SemiBold),
Font(R.font.inter_medium, weight = FontWeight.Medium),
Font(R.font.inter_light, weight = FontWeight.Light),
)
// Set of Material typography styles to start with

View File

@@ -14,7 +14,7 @@ fun SplashView() {
color = MaterialTheme.colors.background
) {
// Image(
// painter = painterResource(MR.images.logo),
// painter = painterResource(R.drawable.logo),
// contentDescription = "Simplex Icon",
// modifier = Modifier
// .height(230.dp)

View File

@@ -128,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}

View File

@@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -31,8 +31,6 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -57,18 +55,18 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1)
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1)
AppBarTitle(stringResource(R.string.create_profile), bottomPadding = DEFAULT_PADDING)
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1)
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1)
Spacer(Modifier.height(DEFAULT_PADDING))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(MR.strings.display_name),
stringResource(R.string.display_name),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Text(
stringResource(MR.strings.no_spaces),
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
@@ -77,7 +75,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(MR.strings.full_name_optional__prompt),
stringResource(R.string.full_name_optional__prompt),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
@@ -87,8 +85,8 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
Row {
if (chatModel.users.isEmpty()) {
SimpleButtonDecorated(
text = stringResource(MR.strings.about_simplex),
icon = painterResource(MR.images.ic_arrow_back_ios_new),
text = stringResource(R.string.about_simplex),
icon = painterResource(R.drawable.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
@@ -106,8 +104,8 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = createColor)
}
}
}

View File

@@ -13,6 +13,7 @@ import android.util.Log
import android.view.ViewGroup
import android.webkit.*
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -23,8 +24,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -41,8 +42,6 @@ import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.NotificationsMode
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
@@ -59,7 +58,7 @@ fun ActiveCallView(chatModel: ChatModel) {
LaunchedEffect(Unit) {
// Start service when call happening since it's not already started.
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
if (!ntfModeService) SimplexService.start()
if (!ntfModeService) SimplexService.start(SimplexApp.context)
}
DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -273,14 +272,14 @@ private fun ActiveCallOverlayLayout(
ToggleAudioButton(call, toggleAudio)
Spacer(Modifier.size(40.dp))
IconButton(onClick = dismiss) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
Icon(painterResource(R.drawable.ic_call_end_filled), stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
}
if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, toggleVideo)
ControlButton(call, painterResource(R.drawable.ic_flip_camera_android_filled), R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, painterResource(R.drawable.ic_videocam_filled), R.string.icon_descr_video_off, toggleVideo)
} else {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo)
ControlButton(call, painterResource(R.drawable.ic_videocam_off), R.string.icon_descr_video_on, toggleVideo)
}
}
}
@@ -298,7 +297,7 @@ private fun ActiveCallOverlayLayout(
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
Icon(painterResource(R.drawable.ic_call_end_filled), stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
}
}
Box(Modifier.padding(start = 32.dp)) {
@@ -316,7 +315,7 @@ private fun ActiveCallOverlayLayout(
}
@Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) {
private fun ControlButton(call: Call, icon: Painter, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) {
IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
@@ -329,18 +328,18 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, a
@Composable
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio)
ControlButton(call, painterResource(R.drawable.ic_mic), R.string.icon_descr_audio_off, toggleAudio)
} else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio)
ControlButton(call, painterResource(R.drawable.ic_mic_off), R.string.icon_descr_audio_on, toggleAudio)
}
}
@Composable
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) {
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled)
ControlButton(call, painterResource(R.drawable.ic_volume_up), R.string.icon_descr_speaker_off, toggleSound, enabled)
} else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled)
ControlButton(call, painterResource(R.drawable.ic_volume_down), R.string.icon_descr_speaker_on, toggleSound, enabled)
}
}

View File

@@ -24,8 +24,8 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -33,10 +33,9 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.OpenChatAction
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.res.MR
import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() {
@@ -171,18 +170,18 @@ fun IncomingCallLockScreenAlertLayout(
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
LockScreenCallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall)
LockScreenCallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall)
LockScreenCallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall)
LockScreenCallButton(stringResource(R.string.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
}
} else if (callOnLockScreen == CallOnLockScreen.SHOW) {
SimpleXLogo()
Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(MR.images.ic_check_filled), click = openApp)
SimpleButton(text = stringResource(R.string.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp)
}
}
}
@@ -190,8 +189,8 @@ fun IncomingCallLockScreenAlertLayout(
@Composable
private fun SimpleXLogo() {
Image(
painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo),
contentDescription = stringResource(MR.strings.image_descr_simplex_logo),
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f)

View File

@@ -11,8 +11,8 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
@@ -21,7 +21,6 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.usersettings.ProfilePreview
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -29,7 +28,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = !chatModel.showCallView.value) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallAlertLayout(
invitation,
@@ -60,9 +59,9 @@ fun IncomingCallAlertLayout(
ProfilePreview(profileOf = invitation.contact, size = 64.dp)
}
Row(verticalAlignment = Alignment.CenterVertically) {
CallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall)
CallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall)
CallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall)
CallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
CallButton(stringResource(R.string.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall)
CallButton(stringResource(R.string.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall)
}
}
}
@@ -76,8 +75,8 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
Spacer(Modifier.width(4.dp))
}
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call))
else CallIcon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call))
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call))
else CallIcon(painterResource(R.drawable.ic_call_filled), stringResource(R.string.icon_descr_audio_call))
Spacer(Modifier.width(4.dp))
Text(invitation.callTypeText, color = MaterialTheme.colors.onBackground)
}

View File

@@ -16,7 +16,7 @@ class SoundPlayer {
private var player: MediaPlayer? = null
var playing = false
fun start(scope: CoroutineScope, sound: Boolean) {
fun start(cxt: Context, scope: CoroutineScope, sound: Boolean) {
player?.reset()
player = MediaPlayer().apply {
setAudioAttributes(
@@ -28,7 +28,7 @@ class SoundPlayer {
setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once))
prepare()
}
val vibrator = ContextCompat.getSystemService(SimplexApp.context, Vibrator::class.java)
val vibrator = ContextCompat.getSystemService(cxt, Vibrator::class.java)
val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE)
playing = true
withScope(scope) {

View File

@@ -1,12 +1,13 @@
package chat.simplex.app.views.call
import android.util.Log
import androidx.compose.runtime.Composable
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.*
import chat.simplex.app.model.Contact
import chat.simplex.app.model.User
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -32,9 +33,9 @@ data class Call(
val encryptionStatus: String @Composable get() = when(callState) {
CallState.WaitCapabilities -> ""
CallState.InvitationSent -> stringResource(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
CallState.InvitationAccepted -> stringResource(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
else -> stringResource(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
CallState.InvitationSent -> stringResource(if (localEncrypted) R.string.status_e2e_encrypted else R.string.status_no_e2e_encryption)
CallState.InvitationAccepted -> stringResource(if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_contact_has_e2e_encryption)
else -> stringResource(if (!localEncrypted) R.string.status_no_e2e_encryption else if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_e2e_encrypted)
}
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
@@ -52,15 +53,15 @@ enum class CallState {
Ended;
val text: String @Composable get() = when(this) {
WaitCapabilities -> stringResource(MR.strings.callstate_starting)
InvitationSent -> stringResource(MR.strings.callstate_waiting_for_answer)
InvitationAccepted -> stringResource(MR.strings.callstate_starting)
OfferSent -> stringResource(MR.strings.callstate_waiting_for_confirmation)
OfferReceived -> stringResource(MR.strings.callstate_received_answer)
AnswerReceived -> stringResource(MR.strings.callstate_received_confirmation)
Negotiated -> stringResource(MR.strings.callstate_connecting)
Connected -> stringResource(MR.strings.callstate_connected)
Ended -> stringResource(MR.strings.callstate_ended)
WaitCapabilities -> stringResource(R.string.callstate_starting)
InvitationSent -> stringResource(R.string.callstate_waiting_for_answer)
InvitationAccepted -> stringResource(R.string.callstate_starting)
OfferSent -> stringResource(R.string.callstate_waiting_for_confirmation)
OfferReceived -> stringResource(R.string.callstate_received_answer)
AnswerReceived -> stringResource(R.string.callstate_received_confirmation)
Negotiated -> stringResource(R.string.callstate_connecting)
Connected -> stringResource(R.string.callstate_connected)
Ended -> stringResource(R.string.callstate_ended)
}
}
@@ -98,12 +99,12 @@ sealed class WCallResponse {
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> if (sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
})
val callTitle: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> MR.strings.incoming_video_call
CallMediaType.Audio -> MR.strings.incoming_audio_call
CallMediaType.Video -> R.string.incoming_video_call
CallMediaType.Audio -> R.string.incoming_audio_call
})
}
@Serializable data class CallCapabilities(val encryption: Boolean)
@@ -113,9 +114,9 @@ sealed class WCallResponse {
val remote = remoteCandidate?.candidateType
return when {
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
stringResource(MR.strings.call_connection_peer_to_peer)
stringResource(R.string.call_connection_peer_to_peer)
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
stringResource(MR.strings.call_connection_via_relay)
stringResource(R.string.call_connection_via_relay)
else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
}

View File

@@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@@ -29,13 +29,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.app.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
@@ -83,45 +83,14 @@ fun ChatInfoView(
switchContactAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
connStats.value = chatModel.controller.apiSwitchContact(contact.contactId)
}
})
},
abortSwitchContactAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
}
})
},
syncContactConnection = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
}
},
syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
connStats.value = chatModel.controller.apiAbortSwitchContact(contact.contactId)
}
})
},
@@ -156,9 +125,9 @@ fun ChatInfoView(
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_contact_question),
text = generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning),
confirmText = generalGetString(MR.strings.delete_verb),
title = generalGetString(R.string.delete_contact_question),
text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
@@ -176,9 +145,9 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
text = generalGetString(MR.strings.clear_chat_warning),
confirmText = generalGetString(MR.strings.clear_verb),
title = generalGetString(R.string.clear_chat_question),
text = generalGetString(R.string.clear_chat_warning),
confirmText = generalGetString(R.string.clear_verb),
onConfirm = {
withApi {
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
@@ -209,8 +178,6 @@ fun ChatInfoLayout(
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
abortSwitchContactAddress: () -> Unit,
syncContactConnection: () -> Unit,
syncContactConnectionForce: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
@@ -229,8 +196,8 @@ fun ChatInfoLayout(
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
SectionSpacer()
if (customUserProfile != null) {
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
InfoRow(generalGetString(MR.strings.incognito_random_profile), customUserProfile.chatViewName)
SectionView(generalGetString(R.string.incognito).uppercase()) {
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
}
SectionDividerSpaced()
}
@@ -240,49 +207,45 @@ fun ChatInfoLayout(
VerifyCodeButton(contact.verified, verifyClicked)
}
ContactPreferencesButton(openPreferences)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
} else if (developerTools) {
SynchronizeConnectionButtonForce(syncContactConnectionForce)
}
}
SectionDividerSpaced()
if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
val context = LocalContext.current
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(contact.contactLink) }
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
ShareAddressButton { shareText(context, contact.contactLink) }
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(contact.displayName))
}
SectionDividerSpaced()
}
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.network_status),
generalGetString(R.string.network_status),
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(contactNetworkStatus)
}
if (cStats != null) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
switchAddress = switchContactAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
abortSwitchAddress = abortSwitchContactAddress
)
}
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
}
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
}
@@ -294,9 +257,9 @@ fun ChatInfoLayout(
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), chat.chatInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), chat.chatInfo.apiId.toString())
SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), chat.chatInfo.localDisplayName)
InfoRow(stringResource(R.string.info_row_database_id), chat.chatInfo.apiId.toString())
}
}
SectionBottomSpacer()
@@ -312,7 +275,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (contact.verified) {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
}
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
@@ -351,13 +314,13 @@ fun LocalAliasEditor(
value,
{
Text(
generalGetString(MR.strings.text_field_set_contact_placeholder),
generalGetString(R.string.text_field_set_contact_placeholder),
textAlign = if (center) TextAlign.Center else TextAlign.Start,
color = MaterialTheme.colors.secondary
)
},
leadingIcon = if (leadingIcon) {
{ Icon(painterResource(MR.images.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
{ Icon(painterResource(R.drawable.ic_edit_filled), null, Modifier.padding(start = 7.dp)) }
} else null,
color = MaterialTheme.colors.secondary,
focus = focus,
@@ -392,10 +355,10 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(stringResource(MR.strings.network_status))
Text(stringResource(R.string.network_status))
Icon(
painterResource(MR.images.ic_info),
stringResource(MR.strings.network_status),
painterResource(R.drawable.ic_info),
stringResource(R.string.network_status),
tint = MaterialTheme.colors.primary
)
}
@@ -418,12 +381,12 @@ private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is NetworkStatus.Connected ->
Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = Color.Green)
Icon(painterResource(R.drawable.ic_circle_filled), stringResource(R.string.icon_descr_server_status_connected), tint = Color.Green)
is NetworkStatus.Disconnected ->
Icon(painterResource(MR.images.ic_pending_filled), stringResource(MR.strings.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_pending_filled), stringResource(R.string.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary)
is NetworkStatus.Error ->
Icon(painterResource(MR.images.ic_error_filled), stringResource(MR.strings.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
else -> Icon(painterResource(MR.images.ic_circle), stringResource(MR.strings.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_error_filled), stringResource(R.string.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary)
else -> Icon(painterResource(R.drawable.ic_circle), stringResource(R.string.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary)
}
}
}
@@ -434,7 +397,7 @@ fun SimplexServers(text: String, servers: List<String>) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current
InfoRowEllipsis(text, info) {
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
Toast.makeText(SimplexApp.context, generalGetString(MR.strings.copied), Toast.LENGTH_SHORT).show()
Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show()
}
}
@@ -442,7 +405,7 @@ fun SimplexServers(text: String, servers: List<String>) {
fun SwitchAddressButton(disabled: Boolean, switchAddress: () -> Unit) {
SectionItemView(switchAddress) {
Text(
stringResource(MR.strings.switch_receiving_address),
stringResource(R.string.switch_receiving_address),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
@@ -452,39 +415,17 @@ fun SwitchAddressButton(disabled: Boolean, switchAddress: () -> Unit) {
fun AbortSwitchAddressButton(disabled: Boolean, abortSwitchAddress: () -> Unit) {
SectionItemView(abortSwitchAddress) {
Text(
stringResource(MR.strings.abort_switch_receiving_address),
stringResource(R.string.abort_switch_receiving_address),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
@Composable
fun SynchronizeConnectionButton(syncConnection: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_sync_problem),
stringResource(MR.strings.fix_connection),
click = syncConnection,
textColor = WarningOrange,
iconColor = WarningOrange
)
}
@Composable
fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_warning),
stringResource(MR.strings.renegotiate_encryption),
click = syncConnectionForce,
textColor = Color.Red,
iconColor = Color.Red
)
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem(
if (contactVerified) painterResource(MR.images.ic_verified_user) else painterResource(MR.images.ic_shield),
stringResource(if (contactVerified) MR.strings.view_security_code else MR.strings.verify_security_code),
if (contactVerified) painterResource(R.drawable.ic_verified_user) else painterResource(R.drawable.ic_shield),
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
click = onClick,
iconColor = MaterialTheme.colors.secondary,
)
@@ -493,8 +434,8 @@ fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
@Composable
private fun ContactPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.contact_preferences),
painterResource(R.drawable.ic_toggle_on),
stringResource(R.string.contact_preferences),
click = onClick
)
}
@@ -502,8 +443,8 @@ private fun ContactPreferencesButton(onClick: () -> Unit) {
@Composable
fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_settings_backup_restore),
stringResource(MR.strings.clear_chat_button),
painterResource(R.drawable.ic_settings_backup_restore),
stringResource(R.string.clear_chat_button),
click = onClick,
textColor = WarningOrange,
iconColor = WarningOrange,
@@ -513,8 +454,8 @@ fun ClearChatButton(onClick: () -> Unit) {
@Composable
private fun DeleteContactButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_delete),
stringResource(MR.strings.button_delete_contact),
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_delete_contact),
click = onClick,
textColor = Color.Red,
iconColor = Color.Red,
@@ -524,8 +465,8 @@ private fun DeleteContactButton(onClick: () -> Unit) {
@Composable
fun ShareAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_share_filled),
stringResource(MR.strings.share_address),
painterResource(R.drawable.ic_share_filled),
stringResource(R.string.share_address),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
@@ -540,33 +481,23 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C
fun showSwitchAddressAlert(switchAddress: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.switch_receiving_address_question),
text = generalGetString(MR.strings.switch_receiving_address_desc),
confirmText = generalGetString(MR.strings.change_verb),
title = generalGetString(R.string.switch_receiving_address_question),
text = generalGetString(R.string.switch_receiving_address_desc),
confirmText = generalGetString(R.string.change_verb),
onConfirm = switchAddress
)
}
fun showAbortSwitchAddressAlert(abortSwitchAddress: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.abort_switch_receiving_address_question),
text = generalGetString(MR.strings.abort_switch_receiving_address_desc),
confirmText = generalGetString(MR.strings.abort_switch_receiving_address_confirm),
title = generalGetString(R.string.abort_switch_receiving_address_question),
text = generalGetString(R.string.abort_switch_receiving_address_desc),
confirmText = generalGetString(R.string.abort_switch_receiving_address_confirm),
onConfirm = abortSwitchAddress,
destructive = true,
)
}
fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.sync_connection_force_question),
text = generalGetString(MR.strings.sync_connection_force_desc),
confirmText = generalGetString(MR.strings.sync_connection_force_confirm),
onConfirm = syncConnectionForce,
destructive = true,
)
}
@Preview
@Composable
fun PreviewChatInfoLayout() {
@@ -589,8 +520,6 @@ fun PreviewChatInfoLayout() {
clearChat = {},
switchContactAddress = {},
abortSwitchContactAddress = {},
syncContactConnection = {},
syncContactConnectionForce = {},
verifyClicked = {},
)
}

View File

@@ -12,9 +12,10 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -25,13 +26,13 @@ import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
@Composable
fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
val sent = ci.chatDir.sent
val appColors = CurrentColors.collectAsState().value.appColors
val itemColor = if (sent) appColors.sentMessage else appColors.receivedMessage
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
@Composable
@@ -49,7 +50,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
)
} else {
Text(
generalGetString(MR.strings.item_info_no_text),
generalGetString(R.string.item_info_no_text),
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp, fontStyle = FontStyle.Italic)
)
}
@@ -73,7 +74,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
)
if (current && ci.meta.itemDeleted == null) {
Text(
stringResource(MR.strings.item_info_current),
stringResource(R.string.item_info_current),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
@@ -81,12 +82,12 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
}
if (text != "") {
DefaultDropdownMenu(showMenu) {
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
shareText(text)
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
shareText(context, text)
showMenu.value = false
})
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
copyText(text)
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, text)
showMenu.value = false
})
}
@@ -95,37 +96,37 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
}
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
AppBarTitle(stringResource(if (sent) MR.strings.sent_message else MR.strings.received_message))
AppBarTitle(stringResource(if (sent) R.string.sent_message else R.string.received_message))
SectionView {
InfoRow(stringResource(MR.strings.info_row_sent_at), localTimestamp(ci.meta.itemTs))
InfoRow(stringResource(R.string.info_row_sent_at), localTimestamp(ci.meta.itemTs))
if (!sent) {
InfoRow(stringResource(MR.strings.info_row_received_at), localTimestamp(ci.meta.createdAt))
InfoRow(stringResource(R.string.info_row_received_at), localTimestamp(ci.meta.createdAt))
}
when (val itemDeleted = ci.meta.itemDeleted) {
is CIDeleted.Deleted ->
if (itemDeleted.deletedTs != null) {
InfoRow(stringResource(MR.strings.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs))
InfoRow(stringResource(R.string.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs))
}
is CIDeleted.Moderated ->
if (itemDeleted.deletedTs != null) {
InfoRow(stringResource(MR.strings.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs))
InfoRow(stringResource(R.string.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs))
}
else -> {}
}
val deleteAt = ci.meta.itemTimed?.deleteAt
if (deleteAt != null) {
InfoRow(stringResource(MR.strings.info_row_disappears_at), localTimestamp(deleteAt))
InfoRow(stringResource(R.string.info_row_disappears_at), localTimestamp(deleteAt))
}
if (devTools) {
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
InfoRow(stringResource(R.string.info_row_database_id), ci.meta.itemId.toString())
InfoRow(stringResource(R.string.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
}
}
val versions = ciInfo.itemVersions
if (versions.isNotEmpty()) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
Text(stringResource(R.string.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
versions.forEachIndexed { i, ciVersion ->
ItemVersionView(ciVersion, current = i == 0)
}
@@ -138,47 +139,47 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
val meta = ci.meta
val sent = ci.chatDir.sent
val shareText = mutableListOf<String>(generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
val shareText = mutableListOf<String>(generalGetString(if (sent) R.string.sent_message else R.string.received_message), "")
shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs)))
shareText.add(String.format(generalGetString(R.string.share_text_sent_at), localTimestamp(meta.itemTs)))
if (!ci.chatDir.sent) {
shareText.add(String.format(generalGetString(MR.strings.share_text_received_at), localTimestamp(meta.createdAt)))
shareText.add(String.format(generalGetString(R.string.share_text_received_at), localTimestamp(meta.createdAt)))
}
when (val itemDeleted = ci.meta.itemDeleted) {
is CIDeleted.Deleted ->
if (itemDeleted.deletedTs != null) {
shareText.add(String.format(generalGetString(MR.strings.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs)))
shareText.add(String.format(generalGetString(R.string.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs)))
}
is CIDeleted.Moderated ->
if (itemDeleted.deletedTs != null) {
shareText.add(String.format(generalGetString(MR.strings.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs)))
shareText.add(String.format(generalGetString(R.string.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs)))
}
else -> {}
}
val deleteAt = ci.meta.itemTimed?.deleteAt
if (deleteAt != null) {
shareText.add(String.format(generalGetString(MR.strings.share_text_disappears_at), localTimestamp(deleteAt)))
shareText.add(String.format(generalGetString(R.string.share_text_disappears_at), localTimestamp(deleteAt)))
}
if (devTools) {
shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId))
shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt))
shareText.add(String.format(generalGetString(R.string.share_text_database_id), meta.itemId))
shareText.add(String.format(generalGetString(R.string.share_text_updated_at), meta.updatedAt))
}
val versions = chatItemInfo.itemVersions
if (versions.isNotEmpty()) {
shareText.add("")
shareText.add(generalGetString(MR.strings.edit_history))
shareText.add(generalGetString(R.string.edit_history))
versions.forEachIndexed { index, itemVersion ->
val ts = localTimestamp(itemVersion.itemVersionTs)
shareText.add("")
shareText.add(
if (index == 0 && ci.meta.itemDeleted == null) {
String.format(generalGetString(MR.strings.current_version_timestamp), ts)
String.format(generalGetString(R.string.current_version_timestamp), ts)
} else {
localTimestamp(itemVersion.itemVersionTs)
}
)
val t = itemVersion.msgContent.text
shareText.add(if (t != "") t else generalGetString(MR.strings.item_info_no_text))
shareText.add(if (t != "") t else generalGetString(R.string.item_info_no_text))
}
}
return shareText.joinToString(separator = "\n")

View File

@@ -20,8 +20,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
@@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
@@ -40,7 +41,6 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.AppBarHeight
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
@@ -166,8 +166,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -260,47 +259,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
updateContactStats = { contact ->
withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
if (r != null) {
chatModel.updateContactConnectionStats(contact, r.first)
}
}
},
updateMemberStats = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
}
}
},
findModelChat = { chatId ->
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
@@ -320,7 +278,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
ModalManager.shared.showModal(endButtons = { ShareButton {
shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
shareText(context, itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
}
@@ -385,12 +343,6 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
addMembers: (GroupInfo) -> Unit,
@@ -435,9 +387,7 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
)
}
}
@@ -471,7 +421,7 @@ fun ChatInfoToolbar(
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add {
ItemAction(stringResource(MR.strings.search_verb).capitalize(Locale.current), painterResource(MR.images.ic_search), onClick = {
ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), painterResource(R.drawable.ic_search), onClick = {
showMenu.value = false
showSearch = true
})
@@ -483,11 +433,11 @@ fun ChatInfoToolbar(
showMenu.value = false
startCall(CallMediaType.Audio)
}) {
Icon(painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary)
Icon(painterResource(R.drawable.ic_call_500), stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
}
}
menuItems.add {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), painterResource(R.drawable.ic_videocam), onClick = {
showMenu.value = false
startCall(CallMediaType.Video)
})
@@ -498,15 +448,15 @@ fun ChatInfoToolbar(
showMenu.value = false
addMembers(chat.chatInfo.groupInfo)
}) {
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
Icon(painterResource(R.drawable.ic_person_add_500), stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary)
}
}
}
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
menuItems.add {
ItemAction(
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled.value) painterResource(R.drawable.ic_notifications_off) else painterResource(R.drawable.ic_notifications),
onClick = {
showMenu.value = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
@@ -520,7 +470,7 @@ fun ChatInfoToolbar(
barButtons.add {
IconButton({ showMenu.value = true }) {
Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary)
Icon(MoreVertFilled, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
}
}
@@ -577,7 +527,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
@Composable
private fun ContactVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
}
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
@@ -610,12 +560,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
@@ -731,11 +675,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
} else {
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
} else { // direct message
@@ -746,7 +690,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
@@ -884,8 +828,8 @@ fun BoxWithConstraintsScope.FloatingButtons(
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
ItemAction(
generalGetString(MR.strings.mark_read),
painterResource(MR.images.ic_check),
generalGetString(R.string.mark_read),
painterResource(R.drawable.ic_check),
onClick = {
markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
@@ -992,7 +936,7 @@ private fun bottomEndFloatingButton(
backgroundColor = MaterialTheme.colors.secondaryVariant,
) {
Icon(
painter = painterResource(MR.images.ic_keyboard_arrow_down),
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
contentDescription = null,
tint = MaterialTheme.colors.primary
)
@@ -1032,7 +976,7 @@ private fun providerForGallery(
scrollTo: (Int) -> Unit
): ImageGalleryProvider {
fun canShowMedia(item: ChatItem): Boolean =
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(item.file) != null)
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null)
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
var processedInternalIndex = -skipInternalIndex.sign
@@ -1059,15 +1003,15 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(item.file)
val filePath = getLoadedFilePath(item.file)
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Image(uri, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(item.file)
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
@@ -1169,12 +1113,6 @@ fun PreviewChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
@@ -1237,12 +1175,6 @@ fun PreviewGroupChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },

View File

@@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.res.MR
@Composable
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
@@ -25,8 +24,8 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_draft_filled),
stringResource(MR.strings.icon_descr_file),
painterResource(R.drawable.ic_draft_filled),
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
@@ -37,8 +36,8 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
if (cancelEnabled) {
IconButton(onClick = cancelFile, modifier = Modifier.padding(0.dp)) {
Icon(
painterResource(MR.images.ic_close),
contentDescription = stringResource(MR.strings.icon_descr_cancel_file_preview),
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)

View File

@@ -11,14 +11,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.app.views.helpers.base64ToBitmap
import chat.simplex.res.MR
@Composable
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
@@ -45,7 +44,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
Icon(
painterResource(MR.images.ic_videocam_filled),
painterResource(R.drawable.ic_videocam_filled),
"preview video",
Modifier
.size(20.dp),
@@ -65,8 +64,8 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
if (cancelEnabled) {
IconButton(onClick = cancelImages) {
Icon(
painterResource(MR.images.ic_close),
contentDescription = stringResource(MR.strings.icon_descr_cancel_image_preview),
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_image_preview),
tint = MaterialTheme.colors.primary,
)
}

View File

@@ -27,8 +27,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import chat.simplex.app.*
@@ -36,7 +36,6 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.*
@@ -194,7 +193,7 @@ fun ComposeView(
if (isGranted) {
cameraLauncher.launchWithFallback()
} else {
Toast.makeText(context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show()
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
}
val processPickedMedia = { uris: List<Uri>, text: String? ->
@@ -202,7 +201,7 @@ fun ComposeView(
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
var bitmap: Bitmap? = null
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
when {
isImage -> {
// Image
@@ -210,17 +209,17 @@ fun ComposeView(
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true)
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(uri)
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
@@ -245,16 +244,16 @@ fun ComposeView(
}
val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) {
val fileSize = getFileSize(uri)
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(uri)
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
}
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
}
@@ -382,7 +381,7 @@ fun ComposeView(
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(file)
if (file != null) removeFile(context, file)
return null
}
@@ -461,9 +460,9 @@ fun ComposeView(
is ComposePreview.MediaPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(it.uri)
is UploadContent.Video -> saveFileFromUri(it.uri)
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(context, it.uri)
}
if (file != null) {
files.add(file)
@@ -478,7 +477,7 @@ fun ComposeView(
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderNative.extension, "")))
val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
@@ -487,7 +486,7 @@ fun ComposeView(
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
}
is ComposePreview.FilePreview -> {
val file = saveFileFromUri(preview.uri)
val file = saveFileFromUri(context, preview.uri)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
@@ -595,7 +594,7 @@ fun ComposeView(
suspend fun sendLiveMessage() {
val cs = composeState.value
val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) {
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
@@ -660,10 +659,10 @@ fun ComposeView(
fun contextItemView() {
when (val contextItem = composeState.value.contextItem) {
ComposeContextItem.NoContextItem -> {}
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_reply)) {
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(R.drawable.ic_reply)) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
}
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) {
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(R.drawable.ic_edit_filled)) {
clearState()
}
}
@@ -719,8 +718,8 @@ fun ComposeView(
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.files_and_media_prohibited),
text = generalGetString(MR.strings.only_owners_can_enable_files_and_media)
title = generalGetString(R.string.files_and_media_prohibited),
text = generalGetString(R.string.only_owners_can_enable_files_and_media)
)
}
} else {
@@ -728,8 +727,8 @@ fun ComposeView(
}
IconButton(attachmentClicked, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
Icon(
painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach),
painterResource(R.drawable.ic_attach_file_filled_500),
contentDescription = stringResource(R.string.attach),
tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier
.size(28.dp)
@@ -861,7 +860,7 @@ class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
if (uri != null) uris.add(uri)
}
if (itemCount > 10) {
AlertManager.shared.showAlertMsg(MR.strings.images_limit_title, MR.strings.images_limit_desc)
AlertManager.shared.showAlertMsg(R.string.images_limit_title, R.string.images_limit_desc)
}
uris
}
@@ -888,7 +887,7 @@ class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
if (uri != null) uris.add(uri)
}
if (itemCount > 10) {
AlertManager.shared.showAlertMsg(MR.strings.videos_limit_title, MR.strings.videos_limit_desc)
AlertManager.shared.showAlertMsg(R.string.videos_limit_title, R.string.videos_limit_desc)
}
uris
}

View File

@@ -10,8 +10,8 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -19,7 +19,6 @@ import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
@@ -58,8 +57,8 @@ fun ComposeVoiceView(
enabled = finishedRecording
) {
Icon(
if (audioPlaying.value) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
stringResource(MR.strings.icon_descr_file),
if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
@@ -90,8 +89,8 @@ fun ComposeVoiceView(
modifier = Modifier.padding(0.dp)
) {
Icon(
painterResource(MR.images.ic_close),
contentDescription = stringResource(MR.strings.icon_descr_cancel_file_preview),
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)

View File

@@ -14,13 +14,12 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggle
import chat.simplex.res.MR
@Composable
fun ContactPreferencesView(
@@ -82,7 +81,7 @@ private fun ContactPreferencesLayout(
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.contact_preferences))
AppBarTitle(stringResource(R.string.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
@@ -141,14 +140,14 @@ private fun FeatureSection(
leadingIcon = true,
) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.chat_preferences_you_allow),
generalGetString(R.string.chat_preferences_you_allow),
ContactFeatureAllowed.values(userDefault).map { it to it.text },
allowFeature,
icon = null,
onSelected = onSelected
)
InfoRow(
generalGetString(MR.strings.chat_preferences_contact_allows),
generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text
)
}
@@ -176,13 +175,13 @@ private fun TimedMessagesFeatureSection(
leadingIcon = true,
) {
PreferenceToggle(
generalGetString(MR.strings.chat_preferences_you_allow),
generalGetString(R.string.chat_preferences_you_allow),
checked = allowFeature.value,
) { allow ->
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
}
InfoRow(
generalGetString(MR.strings.chat_preferences_contact_allows),
generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text
)
if (featuresAllowed.timedMessagesAllowed) {
@@ -190,14 +189,14 @@ private fun TimedMessagesFeatureSection(
DropdownCustomTimePickerSettingRow(
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(MR.strings.delete_after),
label = generalGetString(R.string.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues,
customPickerTitle = generalGetString(MR.strings.delete_after),
customPickerConfirmButtonText = generalGetString(MR.strings.custom_time_picker_select),
customPickerTitle = generalGetString(R.string.delete_after),
customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select),
onSelected = onTTLUpdated
)
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(pref.contactPreference.ttl))
InfoRow(generalGetString(R.string.delete_after), timeText(pref.contactPreference.ttl))
}
}
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
@@ -207,19 +206,19 @@ private fun TimedMessagesFeatureSection(
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_contact),
dismissText = generalGetString(MR.strings.exit_without_saving),
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contact),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)

View File

@@ -8,15 +8,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -47,7 +46,7 @@ fun ContextItemView(
.padding(horizontal = 8.dp)
.height(20.dp)
.width(20.dp),
contentDescription = stringResource(MR.strings.icon_descr_context),
contentDescription = stringResource(R.string.icon_descr_context),
tint = MaterialTheme.colors.secondary,
)
MarkdownText(
@@ -59,8 +58,8 @@ fun ContextItemView(
}
IconButton(onClick = cancelContextItem) {
Icon(
painterResource(MR.images.ic_close),
contentDescription = stringResource(MR.strings.cancel_verb),
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.cancel_verb),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
@@ -74,7 +73,7 @@ fun PreviewContextItemView() {
SimpleXTheme {
ContextItemView(
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
contextIcon = painterResource(MR.images.ic_edit_filled)
contextIcon = painterResource(R.drawable.ic_edit_filled)
) {}
}
}

View File

@@ -6,13 +6,12 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
import chat.simplex.res.MR
@Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
@@ -30,7 +29,7 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.scan_code), false)
AppBarTitle(stringResource(R.string.scan_code), false)
Box(
Modifier
.fillMaxWidth()
@@ -43,12 +42,12 @@ private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
close()
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code)
title = generalGetString(R.string.incorrect_code)
)
}
}
}
}
Text(stringResource(MR.strings.scan_code_from_contacts_app))
Text(stringResource(R.string.scan_code_from_contacts_app))
}
}

View File

@@ -48,10 +48,6 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import java.lang.reflect.Field
@@ -100,8 +96,8 @@ fun SendMsgView(
.matchParentSize()
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.observer_cant_send_message_title),
text = generalGetString(MR.strings.observer_cant_send_message_desc)
title = generalGetString(R.string.observer_cant_send_message_title),
text = generalGetString(R.string.observer_cant_send_message_desc)
)
})
)
@@ -161,7 +157,7 @@ fun SendMsgView(
}
else -> {
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward)
val icon = if (cs.editing || cs.liveMessage != null) painterResource(R.drawable.ic_check_filled) else painterResource(R.drawable.ic_arrow_upward)
val disabled = !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
@@ -179,7 +175,7 @@ fun SendMsgView(
) {
menuItems.add {
ItemAction(
generalGetString(MR.strings.send_live_message),
generalGetString(R.string.send_live_message),
BoltFilled,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
@@ -191,8 +187,8 @@ fun SendMsgView(
if (timedMessageAllowed) {
menuItems.add {
ItemAction(
generalGetString(MR.strings.disappearing_message),
painterResource(MR.images.ic_timer),
generalGetString(R.string.disappearing_message),
painterResource(R.drawable.ic_timer),
onClick = {
showCustomDisappearingMessageDialog.value = true
showDropdown.value = false
@@ -234,8 +230,8 @@ private fun CustomDisappearingMessageDialog(
}
CustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
title = generalGetString(R.string.delete_after),
confirmButtonText = generalGetString(R.string.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
@@ -277,13 +273,13 @@ private fun CustomDisappearingMessageDialog(
) {
Text(" ") // centers title
Text(
generalGetString(MR.strings.send_disappearing_message),
generalGetString(R.string.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
painterResource(R.drawable.ic_close),
generalGetString(R.string.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
@@ -291,19 +287,19 @@ private fun CustomDisappearingMessageDialog(
)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
ChoiceButton(generalGetString(R.string.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
ChoiceButton(generalGetString(R.string.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
ChoiceButton(generalGetString(R.string.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
ChoiceButton(generalGetString(R.string.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
@@ -415,14 +411,14 @@ private fun NativeKeyboard(
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
} else if (userIsObserver) {
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
ComposeOverlay(R.string.you_are_observer, textStyle, padding)
}
}
@Composable
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
Text(
generalGetString(textId),
Modifier.padding(padding),
@@ -437,7 +433,7 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>)
{ composeState.value = composeState.value.copy(message = "") },
Modifier.align(Alignment.TopEnd).size(36.dp)
) {
Icon(painterResource(MR.images.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary)
}
}
@@ -494,8 +490,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
Icon(
painterResource(MR.images.ic_keyboard_voice),
stringResource(MR.strings.icon_descr_record_voice_message),
painterResource(R.drawable.ic_keyboard_voice),
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(36.dp)
@@ -508,8 +504,8 @@ private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
painterResource(MR.images.ic_keyboard_voice_filled),
stringResource(MR.strings.icon_descr_record_voice_message),
painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
@@ -540,8 +536,8 @@ private fun LockToCurrentOrientationUntilDispose() {
private fun StopRecordButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
painterResource(MR.images.ic_stop_filled),
stringResource(MR.strings.icon_descr_record_voice_message),
painterResource(R.drawable.ic_stop_filled),
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
@@ -554,8 +550,8 @@ private fun StopRecordButton(onClick: () -> Unit) {
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
Icon(
painterResource(MR.images.ic_keyboard_voice_filled),
stringResource(MR.strings.icon_descr_record_voice_message),
painterResource(R.drawable.ic_keyboard_voice_filled),
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
@@ -575,8 +571,8 @@ private fun CancelLiveMessageButton(
) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
painterResource(MR.images.ic_close),
stringResource(MR.strings.icon_descr_cancel_live_message),
painterResource(R.drawable.ic_close),
stringResource(R.string.icon_descr_cancel_live_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
@@ -609,7 +605,7 @@ private fun SendMsgButton(
) {
Icon(
icon,
stringResource(MR.strings.icon_descr_send_message),
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(sizeDp.value.dp)
@@ -638,7 +634,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
) {
Icon(
BoltFilled,
stringResource(MR.strings.icon_descr_send_message),
stringResource(R.string.icon_descr_send_message),
tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier
.size(36.dp)
@@ -689,9 +685,9 @@ private fun startLiveMessage(
start()
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.live_message),
text = generalGetString(MR.strings.send_live_message_desc),
confirmText = generalGetString(MR.strings.send_verb),
title = generalGetString(R.string.live_message),
text = generalGetString(R.string.send_live_message_desc),
confirmText = generalGetString(R.string.send_verb),
onConfirm = {
liveMessageAlertShown.set(true)
start()
@@ -701,22 +697,22 @@ private fun startLiveMessage(
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.allow_voice_messages_question),
text = generalGetString(MR.strings.you_need_to_allow_to_send_voice),
confirmText = generalGetString(MR.strings.allow_verb),
dismissText = generalGetString(MR.strings.cancel_verb),
title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(R.string.cancel_verb),
onConfirm = onConfirm,
)
}
private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.voice_messages_prohibited),
title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString(
if (isDirectChat)
MR.strings.ask_your_contact_to_enable_voice
R.string.ask_your_contact_to_enable_voice
else
MR.strings.only_group_owners_can_enable_voice
R.string.only_group_owners_can_enable_voice
)
)
}

View File

@@ -10,8 +10,9 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -19,7 +20,6 @@ import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.res.MR
@Composable
fun VerifyCodeView(
@@ -61,14 +61,14 @@ private fun VerifyCodeLayout(
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.security_code), false)
AppBarTitle(stringResource(R.string.security_code), false)
val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary)
Text(String.format(stringResource(MR.strings.is_verified), displayName))
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 4.dp).size(22.dp), tint = MaterialTheme.colors.secondary)
Text(String.format(stringResource(R.string.is_verified), displayName))
} else {
Text(String.format(stringResource(MR.strings.is_not_verified), displayName))
Text(String.format(stringResource(R.string.is_not_verified), displayName))
}
}
@@ -86,16 +86,17 @@ private fun VerifyCodeLayout(
maxLines = 20
)
}
val context = LocalContext.current
Box(Modifier.weight(1f)) {
IconButton({ shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(painterResource(MR.images.ic_share_filled), null, tint = MaterialTheme.colors.primary)
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(painterResource(R.drawable.ic_share_filled), null, tint = MaterialTheme.colors.primary)
}
}
Spacer(Modifier.weight(1f))
}
Text(
generalGetString(MR.strings.to_verify_compare),
generalGetString(R.string.to_verify_compare),
Modifier.padding(bottom = DEFAULT_PADDING)
)
@@ -104,20 +105,20 @@ private fun VerifyCodeLayout(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
if (connectionVerified) {
SimpleButton(generalGetString(MR.strings.clear_verification), painterResource(MR.images.ic_shield)) {
SimpleButton(generalGetString(R.string.clear_verification), painterResource(R.drawable.ic_shield)) {
verifyCode(null) {}
}
} else {
SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) {
SimpleButton(generalGetString(R.string.scan_code), painterResource(R.drawable.ic_qr_code)) {
ModalManager.shared.showModal {
ScanCodeView(verifyCode) { }
}
}
SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) {
SimpleButton(generalGetString(R.string.mark_code_verified), painterResource(R.drawable.ic_verified_user)) {
verifyCode(connectionCode) { verified ->
if (!verified) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.incorrect_code)
title = generalGetString(R.string.incorrect_code)
)
}
}

View File

@@ -18,8 +18,8 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalView
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -32,7 +32,6 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.InfoAboutIncognito
import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
@Composable
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
@@ -113,12 +112,12 @@ fun AddGroupMembersLayout(
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.button_add_members))
AppBarTitle(stringResource(R.string.button_add_members))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(MR.strings.group_unsupported_incognito_main_profile_sent),
generalGetString(MR.strings.group_main_profile_sent),
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent),
true
)
Spacer(Modifier.size(DEFAULT_PADDING))
@@ -140,7 +139,7 @@ fun AddGroupMembersLayout(
horizontalArrangement = Arrangement.Center
) {
Text(
stringResource(MR.strings.no_contacts_to_add),
stringResource(R.string.no_contacts_to_add),
Modifier.padding(),
color = MaterialTheme.colors.secondary
)
@@ -149,7 +148,7 @@ fun AddGroupMembersLayout(
SectionView {
if (creatingGroup) {
SectionItemView(openPreferences) {
Text(stringResource(MR.strings.set_group_preferences))
Text(stringResource(R.string.set_group_preferences))
}
}
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
@@ -163,7 +162,7 @@ fun AddGroupMembersLayout(
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
}
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.select_contacts)) {
SectionView(stringResource(R.string.select_contacts)) {
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText, selectedContacts.size)
}
@@ -180,7 +179,7 @@ private fun SearchRowView(
selectedContactsSize: Int
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_search), stringResource(android.R.string.search_go), tint = MaterialTheme.colors.secondary)
}
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {
@@ -202,7 +201,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
) {
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text }
ExposedDropDownSettingRow(
generalGetString(MR.strings.new_member_role),
generalGetString(R.string.new_member_role),
values,
selectedRole,
icon = null,
@@ -214,8 +213,8 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
@Composable
fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
SettingsActionItem(
painterResource(MR.images.ic_check),
stringResource(MR.strings.invite_to_group_button),
painterResource(R.drawable.ic_check),
stringResource(R.string.invite_to_group_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
@@ -226,8 +225,8 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
@Composable
fun SkipInvitingButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_check),
stringResource(MR.strings.skip_inviting_button),
painterResource(R.drawable.ic_check),
stringResource(R.string.skip_inviting_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
@@ -243,7 +242,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
) {
if (selectedContactsCount >= 1) {
Text(
String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount),
String.format(generalGetString(R.string.num_contacts_selected), selectedContactsCount),
color = MaterialTheme.colors.secondary,
fontSize = 12.sp
)
@@ -251,14 +250,14 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
Modifier.clickable { if (enabled) clearSelection() }
) {
Text(
stringResource(MR.strings.clear_contacts_selection_button),
stringResource(R.string.clear_contacts_selection_button),
color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontSize = 12.sp
)
}
} else {
Text(
stringResource(MR.strings.no_contacts_selected),
stringResource(R.string.no_contacts_selected),
color = MaterialTheme.colors.secondary,
fontSize = 12.sp
)
@@ -299,13 +298,13 @@ fun ContactCheckRow(
val icon: Painter
val iconColor: Color
if (prohibitedToInviteIncognito) {
icon = painterResource(MR.images.ic_theater_comedy_filled)
icon = painterResource(R.drawable.ic_theater_comedy_filled)
iconColor = MaterialTheme.colors.secondary
} else if (checked) {
icon = painterResource(MR.images.ic_check_circle_filled)
icon = painterResource(R.drawable.ic_check_circle_filled)
iconColor = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
} else {
icon = painterResource(MR.images.ic_circle)
icon = painterResource(R.drawable.ic_circle)
iconColor = MaterialTheme.colors.secondary
}
SectionItemView(
@@ -329,7 +328,7 @@ fun ContactCheckRow(
Spacer(Modifier.fillMaxWidth().weight(1f))
Icon(
icon,
contentDescription = stringResource(MR.strings.icon_descr_contact_checked),
contentDescription = stringResource(R.string.icon_descr_contact_checked),
tint = iconColor
)
}
@@ -337,9 +336,9 @@ fun ContactCheckRow(
fun showProhibitedToInviteIncognitoAlertDialog() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invite_prohibited),
text = generalGetString(MR.strings.invite_prohibited_description),
confirmText = generalGetString(MR.strings.ok),
title = generalGetString(R.string.invite_prohibited),
text = generalGetString(R.string.invite_prohibited_description),
confirmText = generalGetString(R.string.ok),
)
}

View File

@@ -17,8 +17,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
@@ -34,7 +34,6 @@ import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert
import chat.simplex.app.views.chatlist.setGroupMembers
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
import chat.simplex.res.MR
@Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
@@ -61,8 +60,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
},
showMemberInfo = { member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -110,12 +108,12 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
val alertTextKey =
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning
else MR.strings.delete_group_for_self_cannot_undo_warning
if (groupInfo.membership.memberCurrent) R.string.delete_group_for_all_members_cannot_undo_warning
else R.string.delete_group_for_self_cannot_undo_warning
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_group_question),
title = generalGetString(R.string.delete_group_question),
text = generalGetString(alertTextKey),
confirmText = generalGetString(MR.strings.delete_verb),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
@@ -133,9 +131,9 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.leave_group_question),
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button),
title = generalGetString(R.string.leave_group_question),
text = generalGetString(R.string.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(R.string.leave_group_button),
onConfirm = {
withApi {
chatModel.controller.leaveGroup(groupInfo.groupId)
@@ -185,10 +183,10 @@ fun GroupChatInfoLayout(
}
GroupPreferencesButton(openPreferences)
}
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
SectionTextFooter(stringResource(R.string.only_group_owners_can_change_prefs))
SectionDividerSpaced(maxTopPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
@@ -201,7 +199,7 @@ fun GroupChatInfoLayout(
AddMembersButton(tint, onAddMembersClick)
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
val filteredMembers = derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } }
if (members.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText)
@@ -225,9 +223,9 @@ fun GroupChatInfoLayout(
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(R.string.info_row_database_id), groupInfo.apiId.toString())
}
}
SectionBottomSpacer()
@@ -261,8 +259,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
@Composable
private fun GroupPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.group_preferences),
painterResource(R.drawable.ic_toggle_on),
stringResource(R.string.group_preferences),
click = onClick
)
}
@@ -270,8 +268,8 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
@Composable
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add),
stringResource(MR.strings.button_add_members),
painterResource(R.drawable.ic_add),
stringResource(R.string.button_add_members),
onClick,
iconColor = tint,
textColor = tint
@@ -315,7 +313,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
)
}
val s = member.memberStatus.shortText
val statusDescr = if (user) String.format(generalGetString(MR.strings.group_info_member_you), s) else s
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
Text(
statusDescr,
color = MaterialTheme.colors.secondary,
@@ -334,14 +332,14 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
@Composable
private fun MemberVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
}
@Composable
private fun GroupLinkButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_link),
stringResource(MR.strings.group_link),
painterResource(R.drawable.ic_link),
stringResource(R.string.group_link),
onClick,
iconColor = MaterialTheme.colors.secondary
)
@@ -350,8 +348,8 @@ private fun GroupLinkButton(onClick: () -> Unit) {
@Composable
private fun CreateGroupLinkButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.create_group_link),
painterResource(R.drawable.ic_add_link),
stringResource(R.string.create_group_link),
onClick,
iconColor = MaterialTheme.colors.secondary
)
@@ -360,8 +358,8 @@ private fun CreateGroupLinkButton(onClick: () -> Unit) {
@Composable
fun EditGroupProfileButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_edit),
stringResource(MR.strings.button_edit_group_profile),
painterResource(R.drawable.ic_edit),
stringResource(R.string.button_edit_group_profile),
onClick,
iconColor = MaterialTheme.colors.secondary
)
@@ -370,12 +368,12 @@ fun EditGroupProfileButton(onClick: () -> Unit) {
@Composable
private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
val text = if (welcomeMessage == null) {
stringResource(MR.strings.button_add_welcome_message)
stringResource(R.string.button_add_welcome_message)
} else {
stringResource(MR.strings.button_welcome_message)
stringResource(R.string.button_welcome_message)
}
SettingsActionItem(
painterResource(MR.images.ic_maps_ugc),
painterResource(R.drawable.ic_maps_ugc),
text,
onClick,
iconColor = MaterialTheme.colors.secondary
@@ -385,8 +383,8 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit
@Composable
private fun LeaveGroupButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_logout),
stringResource(MR.strings.button_leave_group),
painterResource(R.drawable.ic_logout),
stringResource(R.string.button_leave_group),
onClick,
iconColor = Color.Red,
textColor = Color.Red
@@ -396,8 +394,8 @@ private fun LeaveGroupButton(onClick: () -> Unit) {
@Composable
private fun DeleteGroupButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_delete),
stringResource(MR.strings.button_delete_group),
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_delete_group),
onClick,
iconColor = Color.Red,
textColor = Color.Red
@@ -409,7 +407,7 @@ private fun SearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_search), stringResource(android.R.string.search_go), tint = MaterialTheme.colors.secondary)
}
Spacer(Modifier.width(14.dp))
SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) {

View File

@@ -10,8 +10,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
@@ -19,13 +20,13 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.res.MR
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() {
creatingLink = true
withApi {
@@ -49,7 +50,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
groupLinkMemberRole,
creatingLink,
createLink = ::createLink,
share = { shareText(groupLink ?: return@GroupLinkLayout) },
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
@@ -65,9 +66,9 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
},
deleteLink = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_link_question),
text = generalGetString(MR.strings.all_group_members_will_remain_connected),
confirmText = generalGetString(MR.strings.delete_verb),
title = generalGetString(R.string.delete_link_question),
text = generalGetString(R.string.all_group_members_will_remain_connected),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
@@ -101,9 +102,9 @@ fun GroupLinkLayout(
Modifier
.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.group_link))
AppBarTitle(stringResource(R.string.group_link))
Text(
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect),
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 12.dp),
lineHeight = 22.sp
)
@@ -113,7 +114,7 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly
) {
if (groupLink == null) {
SimpleButton(stringResource(MR.strings.button_create_group_link), icon = painterResource(MR.images.ic_add_link), disabled = creatingLink, click = createLink)
SimpleButton(stringResource(R.string.button_create_group_link), icon = painterResource(R.drawable.ic_add_link), disabled = creatingLink, click = createLink)
} else {
RoleSelectionRow(groupInfo, groupLinkMemberRole)
var initialLaunch by remember { mutableStateOf(true) }
@@ -130,13 +131,13 @@ fun GroupLinkLayout(
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) {
SimpleButton(
stringResource(MR.strings.share_link),
icon = painterResource(MR.images.ic_share),
stringResource(R.string.share_link),
icon = painterResource(R.drawable.ic_share),
click = share
)
SimpleButton(
stringResource(MR.strings.delete_link),
icon = painterResource(MR.images.ic_delete),
stringResource(R.string.delete_link),
icon = painterResource(R.drawable.ic_delete),
color = Color.Red,
click = deleteLink
)
@@ -156,7 +157,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
) {
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
ExposedDropDownSettingRow(
generalGetString(MR.strings.initial_member_role),
generalGetString(R.string.initial_member_role),
values,
selectedRole,
icon = null,

View File

@@ -17,8 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -31,7 +31,6 @@ import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -102,46 +101,14 @@ fun GroupMemberInfoView(
switchMemberAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
connStats.value = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
})
},
abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
})
},
syncMemberConnection = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
},
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
connStats.value = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
})
},
@@ -177,9 +144,9 @@ fun GroupMemberInfoView(
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_member),
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
title = generalGetString(R.string.button_remove_member),
text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(R.string.remove_member_confirmation),
onConfirm = {
withApi {
val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId)
@@ -208,8 +175,6 @@ fun GroupMemberInfoLayout(
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
abortSwitchMemberAddress: () -> Unit,
syncMemberConnection: () -> Unit,
syncMemberConnectionForce: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
@@ -246,11 +211,6 @@ fun GroupMemberInfoLayout(
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncMemberConnection)
} else if (developerTools) {
SynchronizeConnectionButtonForce(syncMemberConnectionForce)
}
}
SectionDividerSpaced()
}
@@ -258,9 +218,9 @@ fun GroupMemberInfoLayout(
if (member.contactLink != null) {
val context = LocalContext.current
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(member.contactLink) }
ShareAddressButton { shareText(context, member.contactLink) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
@@ -268,47 +228,47 @@ fun GroupMemberInfoLayout(
} else {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(member.displayName))
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(member.displayName))
}
SectionDividerSpaced()
}
SectionView(title = stringResource(MR.strings.member_info_section_title_member)) {
InfoRow(stringResource(MR.strings.info_row_group), groupInfo.displayName)
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) {
RoleSelectionRow(roles, newRole, onRoleSelected)
} else {
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
}
val conn = member.activeConn
if (conn != null) {
val connLevelDesc =
if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct)
else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc)
if (conn.connLevel == 0) stringResource(R.string.conn_level_desc_direct)
else String.format(generalGetString(R.string.conn_level_desc_indirect), conn.connLevel)
InfoRow(stringResource(R.string.info_row_connection), connLevelDesc)
}
}
if (cStats != null) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
switchAddress = switchMemberAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
abortSwitchAddress = abortSwitchMemberAddress
)
}
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
if (rcvServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
}
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
if (sndServers.isNotEmpty()) {
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
}
@@ -322,9 +282,9 @@ fun GroupMemberInfoLayout(
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString())
SectionView(title = stringResource(R.string.section_title_for_console)) {
InfoRow(stringResource(R.string.info_row_local_name), member.localDisplayName)
InfoRow(stringResource(R.string.info_row_database_id), member.groupMemberId.toString())
}
}
SectionBottomSpacer()
@@ -340,7 +300,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_verified_user), null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = MaterialTheme.colors.secondary)
}
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
@@ -363,8 +323,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
@Composable
fun RemoveMemberButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_delete),
stringResource(MR.strings.button_remove_member),
painterResource(R.drawable.ic_delete),
stringResource(R.string.button_remove_member),
click = onClick,
textColor = Color.Red,
iconColor = Color.Red,
@@ -374,8 +334,8 @@ fun RemoveMemberButton(onClick: () -> Unit) {
@Composable
fun OpenChatButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_chat),
stringResource(MR.strings.button_send_direct_message),
painterResource(R.drawable.ic_chat),
stringResource(R.string.button_send_direct_message),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
@@ -385,8 +345,8 @@ fun OpenChatButton(onClick: () -> Unit) {
@Composable
fun ConnectViaAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_link),
stringResource(MR.strings.connect_button),
painterResource(R.drawable.ic_link),
stringResource(R.string.connect_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
@@ -406,7 +366,7 @@ private fun RoleSelectionRow(
) {
val values = remember { roles.map { it to it.text } }
ExposedDropDownSettingRow(
generalGetString(MR.strings.change_role),
generalGetString(R.string.change_role),
values,
selectedRole,
icon = null,
@@ -423,12 +383,12 @@ private fun updateMemberRoleDialog(
onConfirm: () -> Unit
) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_member_role_question),
title = generalGetString(R.string.change_member_role_question),
text = if (member.memberCurrent)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
String.format(generalGetString(R.string.member_role_will_be_changed_with_notification), newRole.text)
else
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text),
confirmText = generalGetString(MR.strings.change_verb),
String.format(generalGetString(R.string.member_role_will_be_changed_with_invitation), newRole.text),
confirmText = generalGetString(R.string.change_verb),
onDismiss = onDismiss,
onConfirm = onConfirm,
onDismissRequest = onDismiss
@@ -453,8 +413,6 @@ fun PreviewGroupMemberInfoLayout() {
onRoleSelected = {},
switchMemberAddress = {},
abortSwitchMemberAddress = {},
syncMemberConnection = {},
syncMemberConnectionForce = {},
verifyClicked = {},
)
}

View File

@@ -13,13 +13,12 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.res.MR
@Composable
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
@@ -72,7 +71,7 @@ private fun GroupPreferencesLayout(
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.group_preferences))
AppBarTitle(stringResource(R.string.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
@@ -150,10 +149,10 @@ private fun FeatureSection(
DropdownCustomTimePickerSettingRow(
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(MR.strings.delete_after),
label = generalGetString(R.string.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues,
customPickerTitle = generalGetString(MR.strings.delete_after),
customPickerConfirmButtonText = generalGetString(MR.strings.custom_time_picker_select),
customPickerTitle = generalGetString(R.string.delete_after),
customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select),
onSelected = onTTLUpdated
)
}
@@ -165,7 +164,7 @@ private fun FeatureSection(
iconTint = iconTint,
)
if (timedOn) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(preferences.timedMessages.ttl))
InfoRow(generalGetString(R.string.delete_after), timeText(preferences.timedMessages.ttl))
}
}
}
@@ -176,19 +175,19 @@ private fun FeatureSection(
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
Text(stringResource(R.string.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving),
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)

View File

@@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -28,7 +28,6 @@ import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.*
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -106,7 +105,7 @@ fun GroupProfileLayout(
Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING)
) {
ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center)
ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
Box(
Modifier
.fillMaxWidth()
@@ -125,13 +124,13 @@ fun GroupProfileLayout(
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(MR.strings.group_display_name_field),
stringResource(R.string.group_display_name_field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(MR.strings.no_spaces),
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
@@ -140,7 +139,7 @@ fun GroupProfileLayout(
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(MR.strings.group_full_name_field),
stringResource(R.string.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
@@ -149,7 +148,7 @@ fun GroupProfileLayout(
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text(
stringResource(MR.strings.save_group_profile),
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(
groupProfile.copy(
@@ -163,7 +162,7 @@ fun GroupProfileLayout(
)
} else {
Text(
stringResource(MR.strings.save_group_profile),
stringResource(R.string.save_group_profile),
color = MaterialTheme.colors.secondary
)
}
@@ -183,9 +182,9 @@ fun GroupProfileLayout(
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question),
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving),
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)

View File

@@ -14,8 +14,8 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
@@ -24,7 +24,6 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
@@ -75,14 +74,14 @@ private fun GroupWelcomeLayout(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
val editMode = remember { mutableStateOf(true) }
AppBarTitle(stringResource(MR.strings.group_welcome_title))
AppBarTitle(stringResource(R.string.group_welcome_title))
val wt = rememberSaveable { welcomeText }
if (groupInfo.canEdit) {
if (editMode.value) {
val focusRequester = remember { FocusRequester() }
TextEditor(
wt,
Modifier.height(140.dp), stringResource(MR.strings.enter_welcome_message),
Modifier.height(140.dp), stringResource(R.string.enter_welcome_message),
focusRequester = focusRequester
)
LaunchedEffect(Unit) {
@@ -99,7 +98,7 @@ private fun GroupWelcomeLayout(
},
wt.value.isEmpty()
)
CopyTextButton { copyText(wt.value) }
CopyTextButton { copyText(SimplexApp.context, wt.value) }
SectionDividerSpaced(maxBottomPadding = false)
SaveButton(
save = save,
@@ -107,7 +106,7 @@ private fun GroupWelcomeLayout(
)
} else {
TextPreview(wt.value, linkMode)
CopyTextButton { copyText(wt.value) }
CopyTextButton { copyText(SimplexApp.context, wt.value) }
}
SectionBottomSpacer()
}
@@ -132,7 +131,7 @@ private fun TextPreview(text: String, linkMode: SimplexLinkMode, markdown: Boole
private fun SaveButton(save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_update_group_profile), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
}
@@ -141,13 +140,13 @@ private fun SaveButton(save: () -> Unit, disabled: Boolean) {
private fun ChangeModeButton(editMode: Boolean, click: () -> Unit, disabled: Boolean) {
SectionItemView(click, disabled = disabled) {
Icon(
painterResource(if (editMode) MR.images.ic_visibility else MR.images.ic_edit),
contentDescription = generalGetString(MR.strings.edit_verb),
painterResource(if (editMode) R.drawable.ic_visibility else R.drawable.ic_edit),
contentDescription = generalGetString(R.string.edit_verb),
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
)
TextIconSpaced()
Text(
stringResource(if (editMode) MR.strings.group_welcome_preview else MR.strings.edit_verb),
stringResource(if (editMode) R.string.group_welcome_preview else R.string.edit_verb),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
@@ -157,20 +156,20 @@ private fun ChangeModeButton(editMode: Boolean, click: () -> Unit, disabled: Boo
private fun CopyTextButton(click: () -> Unit) {
SectionItemView(click) {
Icon(
painterResource(MR.images.ic_content_copy),
contentDescription = generalGetString(MR.strings.copy_verb),
painterResource(R.drawable.ic_content_copy),
contentDescription = generalGetString(R.string.copy_verb),
tint = MaterialTheme.colors.primary,
)
TextIconSpaced()
Text(stringResource(MR.strings.copy_verb), color = MaterialTheme.colors.primary)
Text(stringResource(R.string.copy_verb), color = MaterialTheme.colors.primary)
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_welcome_message_question),
confirmText = generalGetString(MR.strings.save_and_update_group_profile),
dismissText = generalGetString(MR.strings.exit_without_saving),
title = generalGetString(R.string.save_welcome_message_question),
confirmText = generalGetString(R.string.save_and_update_group_profile),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)

View File

@@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.res.MR
@Composable
fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) {
@@ -22,20 +21,20 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
Modifier
.padding(horizontal = 4.dp)
.padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
@Composable fun ConnectingCallIcon() = Icon(painterResource(MR.images.ic_settings_phone), stringResource(MR.strings.icon_descr_call_connecting), tint = SimplexGreen)
@Composable fun ConnectingCallIcon() = Icon(painterResource(R.drawable.ic_settings_phone), stringResource(R.string.icon_descr_call_connecting), tint = SimplexGreen)
when (status) {
CICallStatus.Pending -> if (sent) {
Icon(painterResource(MR.images.ic_call), stringResource(MR.strings.icon_descr_call_pending_sent))
Icon(painterResource(R.drawable.ic_call), stringResource(R.string.icon_descr_call_pending_sent))
} else {
AcceptCallButton(cInfo, acceptCall)
}
CICallStatus.Missed -> Icon(painterResource(MR.images.ic_call), stringResource(MR.strings.icon_descr_call_missed), tint = Color.Red)
CICallStatus.Rejected -> Icon(painterResource(MR.images.ic_call_end), stringResource(MR.strings.icon_descr_call_rejected), tint = Color.Red)
CICallStatus.Missed -> Icon(painterResource(R.drawable.ic_call), stringResource(R.string.icon_descr_call_missed), tint = Color.Red)
CICallStatus.Rejected -> Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_rejected), tint = Color.Red)
CICallStatus.Accepted -> ConnectingCallIcon()
CICallStatus.Negotiated -> ConnectingCallIcon()
CICallStatus.Progress -> Icon(painterResource(MR.images.ic_phone_in_talk_filled), stringResource(MR.strings.icon_descr_call_progress), tint = SimplexGreen)
CICallStatus.Progress -> Icon(painterResource(R.drawable.ic_phone_in_talk_filled), stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen)
CICallStatus.Ended -> Row {
Icon(painterResource(MR.images.ic_call_end), stringResource(MR.strings.icon_descr_call_ended), tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 4.dp))
Icon(painterResource(R.drawable.ic_call_end), stringResource(R.string.icon_descr_call_ended), tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 4.dp))
Text(durationText(duration), color = MaterialTheme.colors.secondary)
}
CICallStatus.Error -> {}
@@ -53,9 +52,9 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
@Composable
fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
if (cInfo is ChatInfo.Direct) {
SimpleButton(stringResource(MR.strings.answer_call), painterResource(MR.images.ic_ring_volume)) { acceptCall(cInfo.contact) }
SimpleButton(stringResource(R.string.answer_call), painterResource(R.drawable.ic_ring_volume)) { acceptCall(cInfo.contact) }
} else {
Icon(painterResource(MR.images.ic_ring_volume), stringResource(MR.strings.answer_call), tint = MaterialTheme.colors.secondary)
Icon(painterResource(R.drawable.ic_ring_volume), stringResource(R.string.answer_call), tint = MaterialTheme.colors.secondary)
}
// if case let .direct(contact) = chatInfo {
// Button {

View File

@@ -12,7 +12,6 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
@Composable
fun CIFeaturePreferenceView(
@@ -31,7 +30,7 @@ fun CIFeaturePreferenceView(
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null
val acceptTextId = if (setParam) MR.strings.accept_feature_set_1_day else MR.strings.accept_feature
val acceptTextId = if (setParam) R.string.accept_feature_set_1_day else R.string.accept_feature
val param = if (setParam) 86400 else null
val annotatedText = buildAnnotatedString {
withStyle(chatEventStyle) { append(chatItem.content.text + " ") }

View File

@@ -14,8 +14,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -23,7 +23,6 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -33,7 +32,7 @@ fun CIFileView(
receiveFile: (Long) -> Unit
) {
val context = LocalContext.current
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = file)
@Composable
fun fileIcon(
@@ -44,15 +43,15 @@ fun CIFileView(
contentAlignment = Alignment.Center
) {
Icon(
painterResource(MR.images.ic_draft_filled),
stringResource(MR.strings.icon_descr_file),
painterResource(R.drawable.ic_draft_filled),
stringResource(R.string.icon_descr_file),
Modifier.fillMaxSize(),
tint = color
)
if (innerIcon != null) {
Icon(
innerIcon,
stringResource(MR.strings.icon_descr_file),
stringResource(R.string.icon_descr_file),
Modifier
.size(32.dp)
.padding(top = 12.dp),
@@ -77,8 +76,8 @@ fun CIFileView(
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}
@@ -86,21 +85,21 @@ fun CIFileView(
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_file),
generalGetString(MR.strings.file_will_be_received_when_contact_completes_uploading)
generalGetString(R.string.waiting_for_file),
generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_file),
generalGetString(MR.strings.file_will_be_received_when_contact_is_online)
generalGetString(R.string.waiting_for_file),
generalGetString(R.string.file_will_be_received_when_contact_is_online)
)
}
is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(file)
val filePath = getLoadedFilePath(context, file)
if (filePath != null) {
saveFileLauncher.launch(file.fileName)
} else {
Toast.makeText(context, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show()
Toast.makeText(context, generalGetString(R.string.file_not_found), Toast.LENGTH_SHORT).show()
}
}
else -> {}
@@ -152,15 +151,15 @@ fun CIFileView(
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(R.drawable.ic_check_filled))
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvInvitation ->
if (fileSizeValid())
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
fileIcon(innerIcon = painterResource(R.drawable.ic_arrow_downward), color = MaterialTheme.colors.primary)
else
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz))
fileIcon(innerIcon = painterResource(R.drawable.ic_priority_high), color = WarningOrange)
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(R.drawable.ic_more_horiz))
is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
@@ -168,8 +167,8 @@ fun CIFileView(
progressIndicator()
}
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(R.drawable.ic_close))
}
} else {
fileIcon()

View File

@@ -8,7 +8,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.*
@@ -18,7 +18,6 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
@Composable
fun CIGroupInvitationView(
@@ -44,7 +43,7 @@ fun CIGroupInvitationView(
.padding(vertical = 4.dp)
.padding(end = 2.dp)
) {
ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor)
ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = R.drawable.ic_supervised_user_circle_filled, color = iconColor)
Spacer(Modifier.padding(horizontal = 3.dp))
Column(
Modifier.defaultMinSize(minHeight = 60.dp),
@@ -61,11 +60,11 @@ fun CIGroupInvitationView(
@Composable
fun groupInvitationText() {
when {
sent -> Text(stringResource(MR.strings.you_sent_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(MR.strings.you_are_invited_to_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(MR.strings.you_joined_this_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(MR.strings.you_rejected_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(MR.strings.group_invitation_expired))
sent -> Text(stringResource(R.string.you_sent_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(R.string.you_are_invited_to_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(R.string.you_joined_this_group))
!sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(R.string.you_rejected_group_invitation))
!sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(R.string.group_invitation_expired))
}
}
@@ -96,7 +95,7 @@ fun CIGroupInvitationView(
if (action) {
groupInvitationText()
Text(stringResource(
if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join),
if (chatIncognito) R.string.group_invitation_tap_to_join_incognito else R.string.group_invitation_tap_to_join),
color = if (chatIncognito) Indigo else MaterialTheme.colors.primary)
} else {
Box(Modifier.padding(end = 48.dp)) {

View File

@@ -2,6 +2,7 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
@@ -17,8 +18,8 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
@@ -31,8 +32,6 @@ import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.File
@Composable
@@ -53,7 +52,7 @@ fun CIImageView(
}
@Composable
fun fileIcon(icon: Painter, stringId: StringResource) {
fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
@@ -78,14 +77,14 @@ fun CIImageView(
FileProtocol.SMP -> {}
}
is CIFileStatus.SndTransfer -> progressIndicator()
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
else -> {}
}
}
@@ -102,7 +101,7 @@ fun CIImageView(
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
Image(
imageBitmap.asImageBitmap(),
contentDescription = stringResource(MR.strings.image_descr),
contentDescription = stringResource(R.string.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
@@ -119,7 +118,7 @@ fun CIImageView(
fun imageView(painter: Painter, onClick: () -> Unit) {
Image(
painter,
contentDescription = stringResource(MR.strings.image_descr),
contentDescription = stringResource(R.string.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
@@ -140,8 +139,8 @@ fun CIImageView(
}
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
val imageBitmap: Bitmap? = getLoadedImage(file)
val filePath = getLoadedFilePath(file)
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
val filePath = getLoadedFilePath(SimplexApp.context, file)
return imageBitmap to filePath
}
@@ -161,7 +160,7 @@ fun CIImageView(
val view = LocalView.current
imageView(imagePainter, onClick = {
hideKeyboard(view)
if (getLoadedFilePath(file) != null) {
if (getLoadedFilePath(context, file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
@@ -176,21 +175,21 @@ fun CIImageView(
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
CIFileStatus.RcvAccepted ->
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_image),
generalGetString(MR.strings.image_will_be_received_when_contact_completes_uploading)
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_image),
generalGetString(MR.strings.image_will_be_received_when_contact_is_online)
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat.item
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -7,15 +8,15 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
@Composable
fun CIInvalidJSONView(json: String) {
@@ -23,7 +24,7 @@ fun CIInvalidJSONView(json: String) {
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(stringResource(MR.strings.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
}
}
@@ -32,8 +33,9 @@ fun InvalidJSONView(json: String) {
Column {
Spacer(Modifier.height(DEFAULT_PADDING))
SectionView {
SettingsActionItem(painterResource(MR.images.ic_share), generalGetString(MR.strings.share_verb), click = {
shareText(json)
val context = LocalContext.current
SettingsActionItem(painterResource(R.drawable.ic_share), generalGetString(R.string.share_verb), click = {
shareText(context, json)
})
}
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {

View File

@@ -3,7 +3,8 @@ package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.res.painterResource
import chat.simplex.app.R
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -14,7 +15,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -37,11 +37,11 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
// changing this function requires updating reserveSpaceForMeta
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
if (meta.itemEdited) {
StatusIconText(painterResource(MR.images.ic_edit), color)
StatusIconText(painterResource(R.drawable.ic_edit), color)
Spacer(Modifier.width(3.dp))
}
if (meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_timer), color)
StatusIconText(painterResource(R.drawable.ic_timer), color)
val ttl = meta.itemTimed?.ttl
if (ttl != chatTTL) {
Text(shortTimeText(ttl), color = color, fontSize = 12.sp)
@@ -54,7 +54,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
StatusIconText(painterResource(icon), statusColor)
Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
StatusIconText(painterResource(R.drawable.ic_circle_filled), Color.Transparent)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)

View File

@@ -1,231 +1,24 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.text.*
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.Composable
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIRcvDecryptionError(
msgDecryptError: MsgDecryptError,
msgCount: UInt,
cInfo: ChatInfo,
ci: ChatItem,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
showMember: Boolean
) {
LaunchedEffect(Unit) {
if (cInfo is ChatInfo.Direct) {
updateContactStats(cInfo.contact)
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
updateMemberStats(cInfo.groupInfo, ci.chatDir.groupMember)
}
}
@Composable
fun BasicDecryptionErrorItem() {
DecryptionErrorItem(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.decryption_error),
text = alertMessage(msgDecryptError, msgCount)
)
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
CIMsgError(ci, timedMessagesTTL, showMember) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.decryption_error),
text = when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
}
)
}
if (cInfo is ChatInfo.Direct) {
val modelCInfo = findModelChat(cInfo.id)?.chatInfo
if (modelCInfo is ChatInfo.Direct) {
val modelContactStats = modelCInfo.contact.activeConn.connectionStats
if (modelContactStats != null) {
if (modelContactStats.ratchetSyncAllowed) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.fix_connection_question),
text = alertMessage(msgDecryptError, msgCount),
confirmText = generalGetString(MR.strings.fix_connection_confirm),
onConfirm = { syncContactConnection(cInfo.contact) },
)
},
syncSupported = true
)
} else if (!modelContactStats.ratchetSyncSupported) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.fix_connection_not_supported_by_contact),
text = alertMessage(msgDecryptError, msgCount)
)
},
syncSupported = false
)
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
val modelMember = findModelMember(ci.chatDir.groupMember.id)
val modelMemberStats = modelMember?.activeConn?.connectionStats
if (modelMemberStats != null) {
if (modelMemberStats.ratchetSyncAllowed) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.fix_connection_question),
text = alertMessage(msgDecryptError, msgCount),
confirmText = generalGetString(MR.strings.fix_connection_confirm),
onConfirm = { syncMemberConnection(cInfo.groupInfo, modelMember) },
)
},
syncSupported = true
)
} else if (!modelMemberStats.ratchetSyncSupported) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member),
text = alertMessage(msgDecryptError, msgCount)
)
},
syncSupported = false
)
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
}
@Composable
fun DecryptionErrorItemFixButton(
ci: ChatItem,
showMember: Boolean,
onClick: () -> Unit,
syncSupported: Boolean
) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.BottomEnd,
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
Row {
Icon(
painterResource(MR.images.ic_sync_problem),
stringResource(MR.strings.fix_connection),
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(2.dp))
Text(
buildAnnotatedString {
append(generalGetString(MR.strings.fix_connection))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
withStyle(reserveTimestampStyle) { append(" ") } // for icon
},
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
CIMetaView(ci, timedMessagesTTL = null)
}
}
}
@Composable
fun DecryptionErrorItem(
ci: ChatItem,
showMember: Boolean,
onClick: () -> Unit
) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.BottomEnd,
) {
Text(
buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
CIMetaView(ci, timedMessagesTTL = null)
}
}
}
private fun alertMessage(msgDecryptError: MsgDecryptError, msgCount: UInt): String {
return when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(MR.strings.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.RatchetEarlier -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.Other -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
}
}

View File

@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
@@ -16,8 +17,8 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.FileProvider
@@ -29,8 +30,6 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
import com.google.android.exoplayer2.ui.StyledPlayerView
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import java.io.File
@Composable
@@ -47,7 +46,7 @@ fun CIVideoView(
contentAlignment = Alignment.TopEnd
) {
val context = LocalContext.current
val filePath = remember(file) { getLoadedFilePath(file) }
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
val preview = remember(image) { base64ToBitmap(image) }
if (file != null && filePath != null) {
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
@@ -69,14 +68,14 @@ fun CIVideoView(
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_video),
generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading)
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.waiting_for_video),
generalGetString(MR.strings.video_will_be_received_when_contact_is_online)
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
@@ -101,7 +100,7 @@ fun CIVideoView(
@Composable
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
@@ -162,7 +161,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
contentAlignment = Alignment.Center
) {
Icon(
painterResource(MR.images.ic_play_arrow_filled),
painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null,
tint = if (error) WarningOrange else Color.White
)
@@ -190,7 +189,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
color = Color.White
)
/*if (!soundEnabled.value) {
Icon(painterResource(MR.images.ic_volume_off_filled), null,
Icon(painterResource(R.drawable.ic_volume_off_filled), null,
Modifier.padding(start = 5.dp).size(10.dp),
tint = Color.White
)
@@ -221,7 +220,7 @@ private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick:
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
Image(
preview.asImageBitmap(),
contentDescription = stringResource(MR.strings.video_descr),
contentDescription = stringResource(R.string.video_descr),
modifier = Modifier
.width(width)
.combinedClickable(
@@ -253,7 +252,7 @@ private fun progressIndicator() {
}
@Composable
private fun fileIcon(icon: Painter, stringId: StringResource) {
private fun fileIcon(icon: Painter, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
@@ -296,19 +295,19 @@ private fun loadingIndicator(file: CIFile?) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video)
is CIFileStatus.SndComplete -> fileIcon(painterResource(R.drawable.ic_check_filled), R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(R.drawable.ic_arrow_downward), R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(R.drawable.ic_more_horiz), R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else {
progressIndicator()
}
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(R.drawable.ic_close), R.string.icon_descr_file)
else -> {}
}
}
@@ -327,8 +326,8 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
@@ -15,12 +16,12 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.flow.distinctUntilChanged
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@@ -43,7 +44,7 @@ fun CIVoiceView(
) {
if (file != null) {
val context = LocalContext.current
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(file) }
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(context, file) }
var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) }
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
@@ -225,7 +226,7 @@ private fun PlayPauseButton(
contentAlignment = Alignment.Center
) {
Icon(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
if (audioPlaying) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null,
Modifier.size(36.dp),
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
@@ -255,7 +256,7 @@ private fun VoiceMsgIndicator(
if (hasText) {
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
Icon(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
if (audioPlaying) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
contentDescription = null,
Modifier.size(36.dp),
tint = MaterialTheme.colors.primary

View File

@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -14,18 +15,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.IncognitoView
import com.google.accompanist.permissions.rememberPermissionState
import chat.simplex.res.MR
import kotlinx.datetime.Clock
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
@@ -46,12 +49,6 @@ fun ChatItemView(
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
) {
@@ -62,7 +59,7 @@ fun ChatItemView(
val showMenu = remember { mutableStateOf(false) }
val revealed = remember { mutableStateOf(false) }
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
val onLinkLongClick = { _: String -> showMenu.value = true }
val live = composeState.value.liveMessage != null
@@ -75,10 +72,10 @@ fun ChatItemView(
val onClick = {
when (cItem.meta.itemStatus) {
is CIStatus.SndErrorAuth -> {
showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc))
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
}
is CIStatus.SndError -> {
showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
}
else -> {}
}
@@ -122,17 +119,17 @@ fun ChatItemView(
fun deleteMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(MR.strings.delete_message_cannot_be_undone_warning)
generalGetString(R.string.delete_message_cannot_be_undone_warning)
} else {
generalGetString(MR.strings.delete_message_mark_deleted_warning)
generalGetString(R.string.delete_message_mark_deleted_warning)
}
}
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(MR.strings.moderate_message_will_be_deleted_warning)
generalGetString(R.string.moderate_message_will_be_deleted_warning)
} else {
generalGetString(MR.strings.moderate_message_will_be_marked_warning)
generalGetString(R.string.moderate_message_will_be_marked_warning)
}
}
@@ -169,7 +166,7 @@ fun ChatItemView(
MsgReactionsMenu()
}
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = {
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
@@ -178,27 +175,27 @@ fun ChatItemView(
showMenu.value = false
})
}
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
val filePath = getLoadedFilePath(cItem.file)
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
filePath != null -> shareFile(cItem.text, filePath)
else -> shareText(cItem.content.text)
filePath != null -> shareFile(context, cItem.text, filePath)
else -> shareText(context, cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
copyText(cItem.content.text)
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text)
showMenu.value = false
})
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
val filePath = getLoadedFilePath(cItem.file)
val filePath = getLoadedFilePath(context, cItem.file)
if (filePath != null) {
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(cItem.file)
saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
@@ -211,15 +208,15 @@ fun ChatItemView(
}
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = {
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction(
stringResource(MR.strings.hide_verb),
painterResource(MR.images.ic_visibility_off),
stringResource(R.string.hide_verb),
painterResource(R.drawable.ic_visibility_off),
onClick = {
revealed.value = false
showMenu.value = false
@@ -245,8 +242,8 @@ fun ChatItemView(
DefaultDropdownMenu(showMenu) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(MR.strings.reveal_verb),
painterResource(MR.images.ic_visibility),
stringResource(R.string.reveal_verb),
painterResource(R.drawable.ic_visibility),
onClick = {
revealed.value = true
showMenu.value = false
@@ -295,7 +292,7 @@ fun ChatItemView(
fun ModeratedItem() {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage)
DeleteItemAction(cItem, showMenu, questionText = generalGetString(R.string.delete_message_cannot_be_undone_warning), deleteMessage)
}
}
@@ -307,7 +304,7 @@ fun ChatItemView(
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
@@ -347,7 +344,7 @@ fun CancelFileItemAction(
) {
ItemAction(
stringResource(cancelAction.uiActionId),
painterResource(MR.images.ic_close),
painterResource(R.drawable.ic_close),
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
@@ -364,8 +361,8 @@ fun ItemInfoAction(
showMenu: MutableState<Boolean>
) {
ItemAction(
stringResource(MR.strings.info_menu),
painterResource(MR.images.ic_info),
stringResource(R.string.info_menu),
painterResource(R.drawable.ic_info),
onClick = {
showItemDetails(cInfo, cItem)
showMenu.value = false
@@ -382,8 +379,8 @@ fun DeleteItemAction(
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(MR.strings.delete_verb),
painterResource(MR.images.ic_delete),
stringResource(R.string.delete_verb),
painterResource(R.drawable.ic_delete),
onClick = {
showMenu.value = false
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
@@ -400,8 +397,8 @@ fun ModerateItemAction(
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(MR.strings.moderate_verb),
painterResource(MR.images.ic_flag),
stringResource(R.string.moderate_verb),
painterResource(R.drawable.ic_flag),
onClick = {
showMenu.value = false
moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
@@ -464,7 +461,7 @@ fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(MR.strings.delete_message__question),
title = generalGetString(R.string.delete_message__question),
text = questionText,
buttons = {
Row(
@@ -476,13 +473,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
AlertManager.shared.hideAlert()
}) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) }
}) { Text(stringResource(R.string.for_me_only), color = MaterialTheme.colors.error) }
if (chatItem.meta.editable) {
Spacer(Modifier.padding(horizontal = 4.dp))
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
AlertManager.shared.hideAlert()
}) { Text(stringResource(MR.strings.for_everybody), color = MaterialTheme.colors.error) }
}) { Text(stringResource(R.string.for_everybody), color = MaterialTheme.colors.error) }
}
}
}
@@ -491,9 +488,9 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_member_message__question),
title = generalGetString(R.string.delete_member_message__question),
text = questionText,
confirmText = generalGetString(MR.strings.delete_verb),
confirmText = generalGetString(R.string.delete_verb),
destructive = true,
onConfirm = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
@@ -503,7 +500,7 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM
private fun showMsgDeliveryErrorAlert(description: String) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.message_delivery_error_title),
title = generalGetString(R.string.message_delivery_error_title),
text = description,
)
}
@@ -527,12 +524,6 @@ fun PreviewChatItemView() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
)
@@ -556,12 +547,6 @@ fun PreviewChatItemViewDeletedContent() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
)

View File

@@ -14,18 +14,18 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.UriHandler
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock
import kotlin.math.min
@@ -119,7 +119,7 @@ fun FramedItemView(
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image(
imageBitmap,
contentDescription = stringResource(MR.strings.image_descr),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds()
)
@@ -131,7 +131,7 @@ fun FramedItemView(
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image(
imageBitmap,
contentDescription = stringResource(MR.strings.video_descr),
contentDescription = stringResource(R.string.video_descr),
contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds()
)
@@ -141,8 +141,8 @@ fun FramedItemView(
ciQuotedMsgView(qi)
}
Icon(
if (qi.content is MsgContent.MCFile) painterResource(MR.images.ic_draft_filled) else painterResource(MR.images.ic_mic_filled),
if (qi.content is MsgContent.MCFile) stringResource(MR.strings.icon_descr_file) else stringResource(MR.strings.voice_message),
if (qi.content is MsgContent.MCFile) painterResource(R.drawable.ic_draft_filled) else painterResource(R.drawable.ic_mic_filled),
if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message),
Modifier
.padding(top = 6.dp, end = 4.dp)
.size(22.dp),
@@ -182,12 +182,12 @@ fun FramedItemView(
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted != null) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag))
FramedItemHeader(String.format(stringResource(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(R.drawable.ic_flag))
} else {
FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete))
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, painterResource(R.drawable.ic_delete))
}
} else if (ci.meta.isLive) {
FramedItemHeader(stringResource(MR.strings.live), false)
FramedItemHeader(stringResource(R.string.live), false)
}
ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
@@ -300,7 +300,7 @@ fun PriorityLayout(
// Find important element which should tell what max width other elements can use
// Expecting only one such element. Can be less than one but not more
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
val placeables: List<Placeable> = measureable.map {
val placeables: List<Placeable> = measureable.fastMap {
if (it.layoutId == priorityLayoutId)
imagePlaceable!!
else

View File

@@ -19,7 +19,7 @@ import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
@@ -35,7 +35,6 @@ import coil.size.Size
import com.google.accompanist.pager.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
@@ -160,7 +159,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(MR.strings.image_descr),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = modifier,
)
@@ -179,7 +178,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
@Composable
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)

View File

@@ -23,7 +23,6 @@ import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
@Composable
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
@@ -31,21 +30,21 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
when (msgError) {
is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_skipped_messages),
text = generalGetString(MR.strings.alert_text_skipped_messages_it_can_happen_when)
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
is MsgErrorType.MsgBadHash ->
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_msg_bad_hash),
text = generalGetString(MR.strings.alert_text_msg_bad_hash) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers)
title = generalGetString(R.string.alert_title_msg_bad_hash),
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.alert_title_msg_bad_id),
text = generalGetString(MR.strings.alert_text_msg_bad_id) + "\n" +
generalGetString(MR.strings.alert_text_fragment_please_report_to_developers)
title = generalGetString(R.string.alert_title_msg_bad_id),
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
}
}

View File

@@ -19,7 +19,6 @@ import chat.simplex.app.model.CIDeleted
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@Composable
@@ -36,9 +35,9 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
) {
Box(Modifier.weight(1f, false)) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
MarkedDeletedText(String.format(generalGetString(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
} else {
MarkedDeletedText(generalGetString(MR.strings.marked_deleted_description))
MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
}
}
CIMetaView(ci, timedMessagesTTL)

View File

@@ -4,11 +4,11 @@ import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.res.painterResource
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@@ -20,7 +20,6 @@ import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.onboarding.ReadableTextWithLink
import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri
import chat.simplex.res.MR
val bold = SpanStyle(fontWeight = FontWeight.Bold)
@@ -29,14 +28,14 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp)
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
Text(stringResource(R.string.thank_you_for_installing_simplex), lineHeight = 22.sp)
ReadableTextWithLink(R.string.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
Column(
Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
stringResource(MR.strings.to_start_a_new_chat_help_header),
stringResource(R.string.to_start_a_new_chat_help_header),
style = MaterialTheme.typography.h2,
lineHeight = 22.sp
)
@@ -44,33 +43,33 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(stringResource(MR.strings.chat_help_tap_button))
Text(stringResource(R.string.chat_help_tap_button))
Icon(
painterResource(MR.images.ic_person_add),
stringResource(MR.strings.add_contact),
painterResource(R.drawable.ic_person_add),
stringResource(R.string.add_contact),
modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier,
)
Text(stringResource(MR.strings.above_then_preposition_continuation))
Text(stringResource(R.string.above_then_preposition_continuation))
}
Text(annotatedStringResource(MR.strings.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
}
Column(
Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(MR.strings.to_connect_via_link_title), style = MaterialTheme.typography.h2)
Text(stringResource(MR.strings.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(MR.strings.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
Text(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2)
Text(stringResource(R.string.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
}
Column(
Modifier.padding(vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(MR.strings.markdown_in_messages), style = MaterialTheme.typography.h2)
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
MarkdownHelpView()
}
}

Some files were not shown because too many files have changed in this diff Show More