Compare commits

...

27 Commits

Author SHA1 Message Date
Levitating Pineapple 6b8c4ccb89 remove separator animation 2024-09-06 18:12:52 +03:00
Levitating Pineapple d3cc775a80 cleanup 2024-09-06 18:07:18 +03:00
Levitating Pineapple bab13fb355 revert view separation 2024-09-06 18:00:23 +03:00
Levitating Pineapple 459058de88 whitespace 2024-09-06 15:46:25 +03:00
Levitating Pineapple a0f2293174 merge master 2024-09-06 15:44:59 +03:00
Arturs Krumins 06939343a1 ios: revert showing date in chat list timestamp (#4834) 2024-09-06 13:32:41 +01:00
Evgeny Poberezkin 781fd58369 Merge branch 'master' into lp/floating-buttons-date 2024-09-06 12:41:58 +01:00
Evgeny Poberezkin b2ebb81fcf Merge branch 'stable' 2024-09-06 12:40:16 +01:00
Stanislav Dmitrenko bccbb9900f android: fix initializing WorkManager (#4833) 2024-09-06 12:39:23 +01:00
Arturs Krumins 1f3355921c ios: make message information on media readable (#4820)
* ios: ensure legibility of elements rendered over media

* reduce diff

* match meta padding

* material play background

* remove circlebadge

* progress circle

* meta color modes

* refactor

* conditional space

* fix

* fix2

* fix3

* revert video buttons

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-06 12:36:54 +01:00
Evgeny Poberezkin 9e45f68a6e Merge branch 'stable' 2024-09-05 12:06:11 +01:00
Evgeny ebb5d629e7 ios: attach share sheet to the topmost view controller (to support sharing from nested sheets) (#4830) 2024-09-04 23:12:05 +01:00
Diogo e002f33c53 multiplatform: fix deleted contacts with conversations being identified as contact cards (#4828)
* multiplatform: fix deleted contacts with conversations being identified as contact cards

* fix in ios

* Revert "fix in ios"

This reverts commit 9b8c6bc125.

* fix for ios

* same check as ios for android and desktop
2024-09-04 18:12:41 +01:00
Evgeny 71bea947a5 ios: cache base64 images (#4827) 2024-09-04 14:49:01 +01:00
Evgeny Poberezkin 6fe7ef4fc0 Merge branch 'master' into lp/floating-buttons-date 2024-09-03 14:40:21 +01:00
Evgeny Poberezkin 90d5abdff1 Merge branch 'stable' 2024-09-03 14:39:34 +01:00
Stanislav Dmitrenko 6407d5de63 android, desktop: making list items unique (#4812) 2024-09-03 14:38:42 +01:00
Stanislav Dmitrenko 9cba96082d android, desktop: fix crash when window is small in chat view (#4819) 2024-09-03 14:37:04 +01:00
Stanislav Dmitrenko 2089fd8539 android, desktop: remove slash from strings (#4818) 2024-09-03 14:36:41 +01:00
Stanislav Dmitrenko 31c4ff2705 android, desktop: group info remembers scroll position and search (#4817) 2024-09-03 14:36:01 +01:00
sh fe20a43232 flatpak: update metainfo (#4811) 2024-09-02 15:15:16 +01:00
Stanislav Dmitrenko 087519c39d desktop: fix vlc dependency (#4809) 2024-09-02 13:08:09 +01:00
Levitating Pineapple 1bb8d5ea96 background date visibility 2024-08-30 22:23:53 +03:00
Levitating Pineapple 694a865b3f send tuple, reduce lookups 2024-08-30 16:12:20 +03:00
Levitating Pineapple e22660dfd9 revert formatTimestampText 2024-08-29 23:41:32 +03:00
Levitating Pineapple 0656836dd0 floating date separator 2024-08-29 23:11:57 +03:00
Levitating Pineapple 537e3c0b6c ios: add floating date separator 2024-08-29 21:43:18 +03:00
62 changed files with 563 additions and 283 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
if let parsed = UIImage(base64Encoded: image),
if let parsed = imageFromBase64(image),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
@@ -46,7 +46,7 @@ struct CIGroupInvitationView: View {
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -54,7 +54,7 @@ struct CIGroupInvitationView: View {
(
groupInvitationText()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -165,9 +165,9 @@ struct CIImageView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(padding)
}
@@ -16,7 +16,7 @@ struct CILinkView: View {
var body: some View {
VStack(alignment: .center, spacing: 6) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
@@ -18,6 +18,7 @@ struct CIMetaView: View {
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
var invertedMaterial = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
@@ -25,18 +26,59 @@ struct CIMetaView: View {
if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
} else {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
color: chatItem.meta.itemStatus.sndProgress == .partial
? paleMetaColor
: metaColor,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
)
ZStack {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
color: metaColor,
paleColor: paleMetaColor,
colorMode: invertedMaterial
? .invertedMaterial
: .normal,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
).invertedForegroundStyle(enabled: invertedMaterial)
if invertedMaterial {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
colorMode: .normal,
onlyOverrides: true,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
)
}
}
}
}
}
enum MetaColorMode {
// Renders provided colours
case normal
// Fully transparent meta - used for reserving space
case transparent
// Renders white on dark backgrounds and black on light ones
case invertedMaterial
func resolve(_ c: Color?) -> Color? {
switch self {
case .normal: c
case .transparent: .clear
case .invertedMaterial: nil
}
}
var statusSpacer: Text {
switch self {
case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear)
case .invertedMaterial: Text(" ").kerning(13)
}
}
}
@@ -45,47 +87,77 @@ func ciMetaText(
_ meta: CIMeta,
chatTTL: Int?,
encrypted: Bool?,
color: Color = .clear,
color: Color = .clear, // we use this function to reserve space without rendering meta
paleColor: Color? = nil,
primaryColor: Color = .accentColor,
transparent: Bool = false,
colorMode: MetaColorMode = .normal,
onlyOverrides: Bool = false, // only render colors that differ from base
showStatus: Bool = true,
showEdited: Bool = true,
showViaProxy: Bool,
showTimesamp: Bool
) -> Text {
var r = Text("")
var space: Text? = nil
let appendSpace = {
if let sp = space {
r = r + sp
space = nil
}
}
let resolved = colorMode.resolve(color)
if showEdited, meta.itemEdited {
r = r + statusIconText("pencil", color)
r = r + statusIconText("pencil", resolved)
}
if meta.disappearing {
r = r + statusIconText("timer", color).font(.caption2)
r = r + statusIconText("timer", resolved).font(.caption2)
let ttl = meta.itemTimed?.ttl
if ttl != chatTTL {
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
r = r + colored(Text(shortTimeText(ttl)), resolved)
}
r = r + Text(" ")
space = Text(" ")
}
if showViaProxy, meta.sentViaProxy == true {
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
appendSpace()
r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
}
if showStatus {
if let (image, statusColor) = meta.itemStatus.statusIcon(color, primaryColor) {
r = r + Text(image).foregroundColor(transparent ? .clear : statusColor) + Text(" ")
appendSpace()
if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
let metaColor = if onlyOverrides && statusColor == color {
Color.clear
} else {
colorMode.resolve(statusColor)
}
r = r + colored(Text(image), metaColor)
space = Text(" ")
} else if !meta.disappearing {
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
space = colorMode.statusSpacer + Text(" ")
}
}
if let enc = encrypted {
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
appendSpace()
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
space = Text(" ")
}
if showTimesamp {
r = r + meta.timestampText.foregroundColor(color)
appendSpace()
r = r + colored(meta.timestampText, resolved)
}
return r.font(.caption)
}
private func statusIconText(_ icon: String, _ color: Color) -> Text {
Text(Image(systemName: icon)).foregroundColor(color)
private func statusIconText(_ icon: String, _ color: Color?) -> Text {
colored(Text(Image(systemName: icon)), color)
}
// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
private func colored(_ t: Text, _ color: Color?) -> Text {
if let color {
t.foregroundColor(color)
} else {
t
}
}
struct CIMetaView_Previews: PreviewProvider {
@@ -126,7 +126,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
}
.padding(.horizontal, 12)
@@ -145,7 +145,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
@@ -292,30 +292,22 @@ struct CIVideoView: View {
.clipShape(Circle())
}
private func durationProgress() -> some View {
HStack {
Text("\(durationText(videoPlaying ? progress : duration))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding([.top, .leading], 6)
if let file = chatItem.file, !videoPlaying {
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding(.top, 6)
}
private var fileSizeString: String {
if let file = chatItem.file, !videoPlaying {
" " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
} else {
""
}
}
private func durationProgress() -> some View {
Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
.invertedForegroundStyle()
.font(.caption)
.padding(.vertical, 6)
.padding(.horizontal, 12)
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
return ZStack(alignment: .topTrailing) {
@@ -411,9 +403,9 @@ struct CIVideoView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(smallView ? 0 : padding)
}
@@ -428,10 +420,8 @@ struct CIVideoView: View {
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
Circle()
.trim(from: 0, to: Double(progress) / Double(total))
.stroke(
Color(uiColor: .white),
style: StrokeStyle(lineWidth: 2)
)
.stroke(style: StrokeStyle(lineWidth: 2))
.invertedForegroundStyle()
.rotationEffect(.degrees(-90))
.frame(width: 16, height: 16)
.padding([.trailing, .top], smallView ? 0 : 11)
@@ -64,12 +64,17 @@ struct FramedItemView: View {
.overlay(DetermineWidth())
}
if chatItem.content.msgContent != nil {
CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.bottom, 6)
.overlay(DetermineWidth())
.accessibilityLabel("")
if let content = chatItem.content.msgContent {
CIMetaView(
chat: chat,
chatItem: chatItem,
metaColor: theme.colors.secondary,
invertedMaterial: useWhiteMetaColor
)
.padding(.horizontal, 12)
.padding(.bottom, 6)
.overlay(DetermineWidth())
.accessibilityLabel("")
}
}
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
@@ -185,7 +190,7 @@ struct FramedItemView: View {
let v = ZStack(alignment: .topTrailing) {
switch (qi.content) {
case let .image(_, image):
if let uiImage = UIImage(base64Encoded: image) {
if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
@@ -197,7 +202,7 @@ struct FramedItemView: View {
ciQuotedMsgView(qi)
}
case let .video(_, image, _):
if let uiImage = UIImage(base64Encoded: image) {
if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
@@ -85,7 +85,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
@@ -72,7 +72,7 @@ struct ChatItemView: View {
default: nil
}
}
.flatMap { UIImage(base64Encoded: $0) }
.flatMap { imageFromBase64($0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
+139 -58
View File
@@ -77,7 +77,12 @@ struct ChatView: View {
ZStack(alignment: .bottomTrailing) {
chatItemsList()
// TODO: Extract into a separate view, to reduce the scope of `FloatingButtonModel` updates
floatingButtons(unreadBelow: floatingButtonModel.unreadBelow, isNearBottom: floatingButtonModel.isNearBottom)
floatingButtons(
unreadBelow: floatingButtonModel.unreadBelow,
isNearBottom: floatingButtonModel.isNearBottom,
date: floatingButtonModel.date,
isDateVisible: floatingButtonModel.isDateVisible
)
}
connectingText()
if selectedChatItems == nil {
@@ -453,27 +458,60 @@ struct ChatView: View {
static let shared = FloatingButtonModel()
@Published var unreadBelow: Int = 0
@Published var isNearBottom: Bool = true
@Published var date: Date?
@Published var isDateVisible: Bool = false
var isReallyNearBottom: Bool { scrollOffset.value > 0 && scrollOffset.value < 500 }
let visibleItems = PassthroughSubject<[String], Never>()
let listState = PassthroughSubject<ListState, Never>()
let scrollOffset = CurrentValueSubject<Double, Never>(0)
private var bag = Set<AnyCancellable>()
struct ListState: Equatable {
let topItemDate: Date?
let bottomItemId: ChatItem.ID?
}
private struct ViewUpdate: Equatable {
let unreadBelow: Int
let date: Date?
}
init() {
visibleItems
// Date
listState
.receive(on: DispatchQueue.global(qos: .background))
.map { itemIds in
if let viewId = itemIds.first,
let index = ItemsModel.shared.reversedChatItems.firstIndex(where: { $0.viewId == viewId }) {
ItemsModel.shared.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
if chatItem.isRcvNew { unread += 1 }
}
} else { 0 }
.map { listState -> ViewUpdate in
let im = ItemsModel.shared
return ViewUpdate(
unreadBelow: {
if let id = listState.bottomItemId,
let index = im.reversedChatItems.firstIndex(where: { $0.id == id }) {
im.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
if chatItem.isRcvNew { unread += 1 }
}
} else { 0 }
}(),
date: listState.topItemDate.map { Calendar.current.startOfDay(for: $0) }
)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: \.unreadBelow, on: self)
.sink {
self.unreadBelow = $0.unreadBelow
self.date = $0.date
}
.store(in: &bag)
// Date visibility
listState
.removeDuplicates()
.map { _ in self.setDate(visibility: true) }
// Hide the date after 1 second of no scrolling
.debounce(for: 1, scheduler: DispatchQueue.main)
.sink { self.setDate(visibility: false) }
.store(in: &bag)
// Is near bottom
scrollOffset
.map { $0 < 800 }
.removeDuplicates()
@@ -482,49 +520,85 @@ struct ChatView: View {
.assign(to: \.isNearBottom, on: self)
.store(in: &bag)
}
}
private func floatingButtons(unreadBelow: Int, isNearBottom: Bool) -> some View {
VStack {
let unreadAbove = chat.chatStats.unreadCount - unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
func resetDate() {
date = nil
isDateVisible = false
}
private func setDate(visibility isVisible: Bool) {
Task {
if isVisible {
if !isNearBottom,
!isDateVisible,
let date, !Calendar.current.isDateInToday(date) {
await MainActor.run {
isDateVisible = true
}
} label: {
Label("Mark read", systemImage: "checkmark")
}
} else if isDateVisible {
await MainActor.run {
isDateVisible = false
}
}
}
Spacer()
if unreadBelow > 0 {
circleButton {
unreadCountText(unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
}
}
.padding()
}
@ViewBuilder
private func floatingButtons(unreadBelow: Int, isNearBottom: Bool, date: Date?, isDateVisible: Bool) -> some View {
ZStack(alignment: .top) {
if let date {
DateSeparator(date: date)
.padding(.vertical, 4).padding(.horizontal, 8)
.background(.thinMaterial)
.clipShape(Capsule())
.opacity(isDateVisible ? 1 : 0)
}
VStack {
let unreadAbove = chat.chatStats.unreadCount - unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
}
} label: {
Label("Mark read", systemImage: "checkmark")
}
}
}
Spacer()
if unreadBelow > 0 {
circleButton {
unreadCountText(unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .trailing)
}
.onDisappear(perform: floatingButtonModel.resetDate)
}
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
@@ -536,6 +610,21 @@ struct ChatView: View {
}
}
private struct DateSeparator: View {
let date: Date
var body: some View {
Text(String.localizedStringWithFormat(
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
date.formatted(.dateTime.weekday(.abbreviated)),
date.formatted(.dateTime.day().month(.abbreviated))
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
}
private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View {
Button {
CallController.shared.startCall(contact, media)
@@ -704,7 +793,7 @@ struct ChatView: View {
let nextItem = im.reversedChatItems[i - 1]
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
return (
timestamp: largeGap || formatTimestampText(chatItem.meta.itemTs) != formatTimestampText(nextItem.meta.itemTs),
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
largeGap: largeGap,
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
)
@@ -743,15 +832,7 @@ struct ChatView: View {
VStack(spacing: 0) {
chatItemView(chatItem, range, prevItem, timeSeparation)
if let date = timeSeparation.date {
Text(String.localizedStringWithFormat(
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
date.formatted(.dateTime.weekday(.abbreviated)),
date.formatted(.dateTime.day().month(.abbreviated))
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.padding(8)
DateSeparator(date: date).padding(8)
}
}
.overlay {
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
let imgs: [UIImage] = images.compactMap { image in
UIImage(base64Encoded: image)
imageFromBase64(image)
}
if imgs.count == 0 {
ProgressView()
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
HStack(alignment: .center, spacing: 8) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
+25 -18
View File
@@ -204,24 +204,31 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
private func updateVisibleItems() {
let fbm = ChatView.FloatingButtonModel.shared
fbm.scrollOffset.send(tableView.contentOffset.y + InvertedTableView.inset)
fbm.visibleItems.send(
(tableView.indexPathsForVisibleRows ?? [])
.compactMap { indexPath -> String? in
guard let relativeFrame = tableView.superview?.convert(
tableView.rectForRow(at: indexPath),
from: tableView
) else { return nil }
// Checks that the cell is visible accounting for the added insets
let isVisible =
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
return indexPath.item < representer.items.count && isVisible
? representer.items[indexPath.item].viewId
: nil
}
)
if let visibleRows = tableView.indexPathsForVisibleRows,
visibleRows.last?.item ?? 0 < representer.items.count {
let fbm = ChatView.FloatingButtonModel.shared
fbm.scrollOffset.send(tableView.contentOffset.y + InvertedTableView.inset)
fbm.listState.send(.init(
topItemDate: visibleRows
.last { isVisible(indexPath: $0) }
.map { representer.items[$0.item] }?
.meta.itemTs,
bottomItemId: visibleRows
.first { isVisible(indexPath: $0) }
.map { representer.items[$0.item] }?
.id
))
}
}
private func isVisible(indexPath: IndexPath) -> Bool {
if let relativeFrame = tableView.superview?.convert(
tableView.rectForRow(at: indexPath),
from: tableView
) {
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
} else { false }
}
}
@@ -302,7 +302,7 @@ struct ChatPreviewView: View {
case let .link(_, preview):
smallContentPreview(size: dynamicMediaSize) {
ZStack(alignment: .topTrailing) {
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
@@ -323,12 +323,12 @@ struct ChatPreviewView: View {
}
case let .image(_, image):
smallContentPreview(size: dynamicMediaSize) {
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .video(_,image, duration):
smallContentPreview(size: dynamicMediaSize) {
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .voice(_, duration):
@@ -0,0 +1,21 @@
//
// Test.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 31/08/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder
func invertedForegroundStyle(enabled: Bool = true) -> some View {
if enabled {
foregroundStyle(Material.ultraThin)
.environment(\.colorScheme, .dark)
.grayscale(1)
.contrast(-20)
} else { self }
}
}
@@ -20,7 +20,7 @@ struct ProfileImage: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
if let uiImage = UIImage(base64Encoded: imageStr) {
if let uiImage = imageFromBase64(imageStr) {
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
} else {
let c = color.asAnotherColorFromSecondaryVariant(theme)
+11 -5
View File
@@ -11,12 +11,18 @@ import SwiftUI
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
let rootViewController = keyWindow.rootViewController {
// Find the top-most presented view controller
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let completed = completed {
let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
activityViewController.completionWithItemsHandler = handler
}
presentedViewController.present(activityViewController, animated: true)
activityViewController.completionWithItemsHandler = { _, _, _, _ in
completed()
}
}
topController.present(activityViewController, animated: true)
}
}
@@ -196,7 +196,7 @@ func chatContactType(chat: Chat) -> ContactType {
case .contactRequest:
return .request
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
+1 -1
View File
@@ -104,7 +104,7 @@ class ShareModel: ObservableObject {
// Decode base64 images on background thread
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
if let profileImage = chatData.chatInfo.image,
let uiImage = UIImage(base64Encoded: profileImage) {
let uiImage = imageFromBase64(profileImage) {
dict[chatData.id] = uiImage
}
}
+3 -3
View File
@@ -147,8 +147,8 @@ struct ShareView: View {
}
}
@ViewBuilder private func imagePreview(_ img: String) -> some View {
if let img = UIImage(base64Encoded: img) {
@ViewBuilder private func imagePreview(_ imgStr: String) -> some View {
if let img = imageFromBase64(imgStr) {
previewArea {
Image(uiImage: img)
.resizable()
@@ -163,7 +163,7 @@ struct ShareView: View {
@ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View {
previewArea {
HStack(alignment: .center, spacing: 8) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -194,6 +194,7 @@
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; };
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; };
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; };
@@ -532,6 +533,7 @@
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; };
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
@@ -789,6 +791,7 @@
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */,
CE7548092C622630009579B7 /* SwipeLabel.swift */,
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -1489,6 +1492,7 @@
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */,
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */,
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
+29 -14
View File
@@ -2713,7 +2713,7 @@ public struct CIMeta: Decodable, Hashable {
public var deletable: Bool
public var editable: Bool
public var timestampText: Text { get { formatTimestampText(itemTs) } }
public var timestampText: Text { Text(formatTimestampMeta(itemTs)) }
public var recent: Bool { updatedAt + 10 > .now }
public var isLive: Bool { itemLive == true }
public var disappearing: Bool { !isRcvNew && itemTimed?.deleteAt != nil }
@@ -2761,8 +2761,30 @@ public struct CITimed: Decodable, Hashable {
public var deleteAt: Date?
}
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
public func formatTimestampText(_ date: Date) -> Text {
Text(verbatim: date.formatted(date: .omitted, time: .shortened))
Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat))
}
public func formatTimestampMeta(_ date: Date) -> String {
date.formatted(date: .omitted, time: .shortened)
}
private func recent(_ date: Date) -> Bool {
let now = Date()
let calendar = Calendar.current
guard let previousDay = calendar.date(byAdding: DateComponents(day: -1), to: now),
let previousDay18 = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: previousDay),
let currentDay00 = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now),
let currentDay12 = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: now) else {
return false
}
let isSameDay = calendar.isDate(date, inSameDayAs: now)
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
}
public enum CIStatus: Decodable, Hashable {
@@ -2790,13 +2812,14 @@ public enum CIStatus: Decodable, Hashable {
}
}
public func statusIcon(_ metaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
switch self {
case .sndNew: nil
case .sndSent: (Image("checkmark.wide"), metaColor)
case let .sndRcvd(msgRcptStatus, _):
case let .sndSent(sndProgress):
(Image("checkmark.wide"), sndProgress == .partial ? paleMetaColor : metaColor)
case let .sndRcvd(msgRcptStatus, sndProgress):
switch msgRcptStatus {
case .ok: (Image("checkmark.2"), metaColor)
case .ok: (Image("checkmark.2"), sndProgress == .partial ? paleMetaColor : metaColor)
case .badMsgHash: (Image("checkmark.2"), .red)
}
case .sndErrorAuth: (Image(systemName: "multiply"), .red)
@@ -2808,14 +2831,6 @@ public enum CIStatus: Decodable, Hashable {
}
}
public var sndProgress: SndCIStatusProgress? {
switch self {
case let .sndSent(sndProgress): sndProgress
case let .sndRcvd(_ , sndProgress): sndProgress
default: nil
}
}
public var statusInfo: (String, String)? {
switch self {
case .sndNew: return nil
+17 -3
View File
@@ -383,16 +383,30 @@ extension UIImage {
}
return self
}
}
public convenience init?(base64Encoded: String?) {
if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) {
self.init(data: data)
public func imageFromBase64(_ base64Encoded: String?) -> UIImage? {
if let base64Encoded {
if let img = imageCache.object(forKey: base64Encoded as NSString) {
return img
} else if let data = Data(base64Encoded: dropImagePrefix(base64Encoded)),
let img = UIImage(data: data) {
imageCache.setObject(img, forKey: base64Encoded as NSString)
return img
} else {
return nil
}
} else {
return nil
}
}
private var imageCache: NSCache<NSString, UIImage> = {
var cache = NSCache<NSString, UIImage>()
cache.countLimit = 1000
return cache
}()
public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
logger.debug("getLinkMetadata: fetching URL preview")
LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in
@@ -5,9 +5,8 @@ import android.util.Log
import androidx.work.*
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.DBMigrationResult
import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.initChatControllerOnStart
import chat.simplex.common.views.helpers.DatabaseUtils
import kotlinx.coroutines.*
import java.util.Date
@@ -30,12 +29,12 @@ object MessagesFetcherWorker {
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
}
fun cancelAll() {
Log.d(TAG, "Worker: canceled all tasks")
WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG)
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
}
}
@@ -7,7 +7,6 @@ import chat.simplex.common.platform.Log
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.*
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
@@ -37,7 +36,7 @@ import java.util.concurrent.TimeUnit
const val TAG = "SIMPLEX"
class SimplexApp: Application(), LifecycleEventObserver {
class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider {
val chatModel: ChatModel
get() = chatController.chatModel
@@ -164,7 +163,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
.build()
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
}
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
@@ -367,4 +366,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
}
}
// Fix for an exception:
// WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().build()
}
@@ -292,7 +292,7 @@ class SimplexService: Service() {
fun scheduleStart(context: Context) {
Log.d(TAG, "Enqueuing work to start subscriber service")
val workManager = WorkManager.getInstance(context)
val workManager = context.getWorkManagerInstance()
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
}
@@ -6,6 +6,8 @@ import android.net.LocalServerSocket
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity
import androidx.work.Configuration
import androidx.work.WorkManager
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
@@ -72,3 +74,16 @@ fun initHaskell() {
initHS()
}
fun Context.getWorkManagerInstance(): WorkManager {
// https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672
if (!WorkManager.isInitialized()) {
try {
WorkManager.initialize(this, Configuration.Builder().build())
} catch (e: IllegalStateException) {
Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}")
}
}
return WorkManager.getInstance(this)
}
@@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings
import SectionView
import androidx.compose.runtime.Composable
import androidx.work.WorkManager
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
@@ -33,7 +32,7 @@ fun restartApp() {
}
private fun shutdownApp() {
WorkManager.getInstance(androidAppContext).cancelAllWork()
androidAppContext.getWorkManagerInstance().cancelAllWork()
platform.androidServiceSafeStop()
Runtime.getRuntime().exit(0)
}
@@ -411,7 +411,9 @@ object ChatModel {
// remove from current chat
if (chatId.value == cInfo.id) {
chatItems.removeAll {
val remove = it.id == cItem.id
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt
if (remove) { AudioPlayer.stop(it) }
remove
}
@@ -5348,6 +5348,7 @@ abstract class TerminalItem {
val date: Instant = Clock.System.now()
abstract val label: String
abstract val details: String
val createdAtNanos: Long = System.nanoTime()
class Cmd(override val id: Long, override val remoteHostId: Long?, val cmd: CC): TerminalItem() {
override val label get() = "> ${cmd.cmdString}"
@@ -132,7 +132,7 @@ fun TerminalLog() {
}
val clipboard = LocalClipboardManager.current
LazyColumnWithScrollBar(reverseLayout = true) {
items(reversedTerminalItems) { item ->
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId "
Text(
@@ -998,7 +998,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
)
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem ->
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
CompositionLocalProvider(
// Makes horizontal and vertical scrolling to coexist nicely.
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@Composable
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
BackHandler(onBack = close)
// TODO derivedStateOf?
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
@@ -249,9 +249,8 @@ fun AddGroupMembersButton(
)
}
@Composable
fun GroupChatInfoLayout(
fun ModalData.GroupChatInfoLayout(
chat: Chat,
groupInfo: GroupInfo,
currentUser: User,
@@ -272,12 +271,12 @@ fun GroupChatInfoLayout(
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
onSearchClicked: () -> Unit
) {
val listState = rememberLazyListState()
val listState = remember { appBarHandler.listState }
val scope = rememberCoroutineScope()
KeyChangeEffect(chat.id) {
scope.launch { listState.scrollToItem(0) }
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
val filteredMembers = remember(members) {
derivedStateOf {
val s = searchText.value.text.trim().lowercase()
@@ -674,7 +673,7 @@ private fun SearchRowView(
@Composable
fun PreviewGroupChatInfoLayout() {
SimpleXTheme {
GroupChatInfoLayout(
ModalData().GroupChatInfoLayout(
chat = Chat(
remoteHostId = null,
chatInfo = ChatInfo.Direct.sampleData,
@@ -186,7 +186,7 @@ fun ErrorChatListItem() {
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
}
}
@@ -356,7 +356,8 @@ sealed class WallpaperType {
private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap {
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
val drawScope = CanvasDrawScope()
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
// Don't allow to make zero size because it crashes the app when reducing a size of a window on desktop
val bitmap = ImageBitmap(size.width.toInt().coerceAtLeast(1), size.height.toInt().coerceAtLeast(1))
val canvas = Canvas(bitmap)
drawScope.draw(
density = Density(density),
@@ -71,12 +71,6 @@ fun SearchTextField(
}
}
DisposableEffect(Unit) {
onDispose {
if (searchText.value.text.isNotEmpty()) onValueChange("")
}
}
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
textColor = MaterialTheme.colors.onBackground,
@@ -36,11 +36,13 @@ import chat.simplex.common.views.chatlist.ScrollDirection
import chat.simplex.common.views.contacts.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import java.net.URI
@Composable
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
@@ -97,7 +99,7 @@ fun chatContactType(chat: Chat): ContactType {
val contact = cInfo.contact
when {
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD
contact.chatDeleted -> ContactType.CHAT_DELETED
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
else -> ContactType.UNLISTED
@@ -111,10 +113,8 @@ private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>):
return c.filter { chat -> contactTypes.contains(chatContactType(chat)) }
}
private var lazyListState = 0 to 0
@Composable
private fun NewChatSheetLayout(
private fun ModalData.NewChatSheetLayout(
rh: RemoteHostInfo?,
addContact: () -> Unit,
scanPaste: () -> Unit,
@@ -122,7 +122,23 @@ private fun NewChatSheetLayout(
close: () -> Unit,
) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val listState = remember { appBarHandler.listState }
// This is workaround of an issue when position of a list is not restored (when going back to that screen) when a header exists.
// Upon returning back, this code returns correct index and position if number of items is the same
LaunchedEffect(Unit) {
val prevIndex = listState.firstVisibleItemIndex
val prevOffset = listState.firstVisibleItemScrollOffset
val total = listState.layoutInfo.totalItemsCount
if (prevIndex == 0 && prevOffset == 0) return@LaunchedEffect
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
.filter { it == 0 to 0 }
.collect {
if (total <= listState.layoutInfo.totalItemsCount) {
listState.scrollToItem(prevIndex, prevOffset)
}
cancel()
}
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
@@ -536,7 +552,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
}
@Composable
private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
@@ -558,7 +574,7 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
}
}
) { contentPadding ->
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val listState = remember { appBarHandler.listState }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
@@ -723,6 +739,6 @@ fun ActionButton(
@Composable
private fun PreviewNewChatSheet() {
SimpleXTheme {
NewChatSheet(rh = null, close = {})
ModalData().NewChatSheet(rh = null, close = {})
}
}
@@ -22,8 +22,8 @@
<string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string>
<string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيستلم العضو دعوة جديدة.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى "%s". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى "%s". سيستلم العضو دعوة جديدة.</string>
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
<string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string>
@@ -767,7 +767,7 @@
<string name="no_contacts_selected">لم تٌحدد جهات اتصال</string>
<string name="v4_6_group_moderation_descr">يمكّن للمشرف الآن:
\n- حذف رسائل الأعضاء.
\n- تعطيل الأعضاء (دور \"المراقب\")</string>
\n- تعطيل الأعضاء (دور "المراقب")</string>
<string name="settings_notifications_mode_title">خدمة الإشعار</string>
<string name="chat_preferences_off">غير مفعّل</string>`
<string name="chat_preferences_on">مفعل</string>
@@ -1076,7 +1076,7 @@
<string name="stop_file__action">إيقاف الملف</string>
<string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string>
<string name="icon_descr_address">عنوان SimpleX</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]></string>
<string name="share_with_contacts">مشاركة مع جهات الاتصال</string>
<string name="shutdown_alert_question">إيقاف التشغيل؟</string>
<string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string>
@@ -1406,7 +1406,7 @@
<string name="connected_desktop">سطح المكتب متصل</string>
<string name="multicast_connect_automatically">اتصل تلقائيًا</string>
<string name="desktop_address">عنوان سطح المكتب</string>
<string name="marked_deleted_items_description">وضّع علامة \"محذوفة\" على %d من الرسائل</string>
<string name="marked_deleted_items_description">وضّع علامة "محذوفة" على %d من الرسائل</string>
<string name="discover_on_network">اكتشف عبر الشبكة المحلية</string>
<string name="connect_plan_connect_via_link">اتصل عبر الرابط؟</string>
<string name="connect_plan_connect_to_yourself">اتصل بنفسك؟</string>
@@ -1560,8 +1560,8 @@
<string name="change_verb">Change</string>
<string name="switch_verb">Switch</string>
<string name="change_member_role_question">Change group role?</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string>
<string name="connect_via_member_address_alert_title">Connect directly?</string>
<string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string>
<string name="error_removing_member">Error removing member</string>
@@ -1897,7 +1897,7 @@
<string name="v4_6_audio_video_calls">Audio and video calls</string>
<string name="v4_6_audio_video_calls_descr">Support bluetooth and other improvements.</string>
<string name="v4_6_group_moderation">Group moderation</string>
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role)</string>
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members ("observer" role)</string>
<string name="v4_6_group_welcome_message">Group welcome message</string>
<string name="v4_6_group_welcome_message_descr">Set the message shown to new members!</string>
<string name="v4_6_reduced_battery_usage">Further reduced battery usage</string>
@@ -482,7 +482,7 @@
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string>
<string name="send_receipts">Изпращане на потвърждениe за доставка</string>
<string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string>
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.</string>
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</string>
<string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string>
<string name="enter_correct_passphrase">Въведи правилна парола.</string>
<string name="feature_enabled_for_you">активирано за вас</string>
@@ -517,7 +517,7 @@
<string name="snd_conn_event_ratchet_sync_agreed">криптирането е съгласувано за %s</string>
<string name="button_edit_group_profile">Редактирай групов профил</string>
<string name="share_text_disappears_at">Изчезва в: %s</string>
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на \"%s\". Членът ще получи нова покана.</string>
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на "%s". Членът ще получи нова покана.</string>
<string name="conn_level_desc_direct">директна</string>
<string name="renegotiate_encryption">Предоговори криптирането</string>
<string name="group_display_name_field">Въведи име на групата:</string>
@@ -918,7 +918,7 @@
<string name="member_will_be_removed_from_group_cannot_be_undone">Членът ще бъде премахнат от групата - това не може да бъде отменено!</string>
<string name="item_info_no_text">няма текст</string>
<string name="button_remove_member">Острани член</string>
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string>
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на "%s". Всички в групата ще бъдат уведомени.</string>
<string name="users_delete_data_only">Само данни за локален профил</string>
<string name="users_delete_with_connections">Профилни и сървърни връзки</string>
<string name="user_mute">Без звук</string>
@@ -936,7 +936,7 @@
<string name="v4_6_hidden_chat_profiles_descr">Защитете чат профилите с парола!</string>
<string name="v4_6_group_moderation_descr">Сега администраторите могат:
\n- да изтриват съобщения на членове.
\n- да деактивират членове (роля \"наблюдател\")</string>
\n- да деактивират членове (роля "наблюдател")</string>
<string name="v4_6_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string>
<string name="v5_0_polish_interface">Полски интерфейс</string>
<string name="v5_1_message_reactions">Реакции на съобщения</string>
@@ -1155,7 +1155,7 @@
<string name="this_link_is_not_a_valid_connection_link">Този линк не е валиден линк за връзка!</string>
<string name="your_chat_profiles">Вашите чат профили</string>
<string name="smp_servers_per_user">Сървърите за нови връзки на текущия ви чат профил</string>
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\".</string>
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата "Вашите чат профили".</string>
<string name="profile_is_only_shared_with_your_contacts">Профилът се споделя само с вашите контакти.</string>
<string name="delete_files_and_media_desc">Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</string>
<string name="tap_to_activate_profile">Докосни за активиране на профил.</string>
@@ -1646,7 +1646,7 @@
<string name="v5_6_quantum_resistant_encryption">Квантово устойчиво криптиране</string>
<string name="v5_6_app_data_migration">Миграция на данните от приложението</string>
<string name="v5_6_app_data_migration_descr">Мигрирайте към друго устройство чрез QR код.</string>
<string name="v5_6_picture_in_picture_calls">Обаждания \"картина в картина\"</string>
<string name="v5_6_picture_in_picture_calls">Обаждания "картина в картина"</string>
<string name="v5_6_safer_groups">По-безопасни групи</string>
<string name="v5_6_safer_groups_descr">Администраторите могат да блокират член за всички.</string>
<string name="migrate_to_device_downloading_details">Подробности за линка се изтеглят</string>
@@ -462,7 +462,7 @@
<string name="rcv_group_event_member_left">odešel</string>
<string name="clear_contacts_selection_button">Vyčistit</string>
<string name="switch_verb">Přepnout</string>
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
<string name="member_role_will_be_changed_with_notification">Role bude změněna na "%s". Všichni ve skupině budou informováni.</string>
<string name="error_removing_member">Chyba při odebrání člena</string>
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
<string name="network_option_seconds_label">vteřiny</string>
@@ -857,7 +857,7 @@
<string name="role_in_group">Role</string>
<string name="change_role">Změnit roli</string>
<string name="change_verb">Změnit</string>
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na "%s". Člen obdrží novou pozvánku.</string>
<string name="error_changing_role">Chyba při změně role</string>
<string name="conn_level_desc_direct">přímo</string>
<string name="sending_via">Odesíláno přes</string>
@@ -972,7 +972,7 @@
<string name="v4_6_reduced_battery_usage_descr">Další vylepšení již brzy!</string>
<string name="v4_6_group_moderation_descr">Nyní mohou správci:
\n- mazat zprávy členů.
\n- zakázat členy (role \"pozorovatel\")</string>
\n- zakázat členy (role "pozorovatel")</string>
<string name="save_profile_password">Uložit heslo profilu</string>
<string name="user_mute">Ztlumit</string>
<string name="v4_6_hidden_chat_profiles_descr">Chraňte své chat profily heslem!</string>
@@ -792,8 +792,8 @@
<string name="change_verb">Ändern</string>
<string name="switch_verb">Wechseln</string>
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf "%s" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf "%s" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
<string name="info_row_group">Gruppe</string>
@@ -1074,7 +1074,7 @@
<string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string>
<string name="group_welcome_title">Begrüßungsmeldung</string>
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten für das Menü gedrückt halten.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Ihre Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite "Ihre Chat-Profile" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
@@ -1151,7 +1151,7 @@
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
<string name="la_lock_mode">SimpleX-Sperrmodus</string>
<string name="lock_not_enabled">SimpleX-Sperre ist nicht aktiviert!</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf "Nein", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
<string name="submit_passcode">Bestätigen</string>
<string name="la_mode_system">System</string>
<string name="la_could_not_be_verified">Sie können nicht überprüft werden bitte versuchen Sie es nochmal.</string>
@@ -726,7 +726,7 @@
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
<string name="set_contact_name">Escribe un nombre para el contacto</string>
<string name="unknown_error">Error desconocido</string>
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a \"%s\" y se notificará al grupo.</string>
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a "%s" y se notificará al grupo.</string>
<string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string>
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
@@ -814,7 +814,7 @@
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
<string name="switch_verb">Cambiar</string>
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a \"%s\" y recibirá una invitación nueva.</string>
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a "%s" y recibirá una invitación nueva.</string>
<string name="update_network_settings_confirmation">Actualizar</string>
<string name="update_network_settings_question">¿Actualizar la configuración de red\?</string>
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
@@ -992,7 +992,7 @@
<string name="you_will_still_receive_calls_and_ntfs">Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</string>
<string name="v4_6_group_moderation_descr">Ahora los administradores pueden:
\n- eliminar mensajes de los miembros.
\n- desactivar el rol miembro (a rol \"observador\")</string>
\n- desactivar el rol miembro (a rol "observador")</string>
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles.</string>
<string name="database_upgrade">Actualización de la base de datos</string>
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
@@ -757,7 +757,7 @@
<string name="v4_6_reduced_battery_usage_descr">Lisää parannuksia on tulossa pian!</string>
<string name="v4_6_group_moderation_descr">Nyt järjestelmänvalvojat voivat:
\n- poistaa jäsenten viestit.
\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)</string>
\n- poista jäsenet käytöstä ("tarkkailija" rooli)</string>
<string name="save_and_notify_group_members">Tallenna ja ilmoita ryhmän jäsenille</string>
<string name="stop_chat_confirmation">Lopeta</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</string>
@@ -1108,8 +1108,8 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi.</string>
<string name="icon_descr_video_call">videopuhelu</string>
<string name="group_info_section_title_num_members"> %1$s JÄSENET</string>
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon \"%s\". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon \"%s\". Jäsen saa uuden kutsun.</string>
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun.</string>
<string name="voice_prohibited_in_this_chat">Ääniviestit ovat kiellettyjä tässä keskustelussa.</string>
<string name="v4_3_voice_messages">Ääniviestit</string>
<string name="trying_to_connect_to_server_to_receive_messages">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</string>
@@ -972,7 +972,7 @@
<string name="v4_6_hidden_chat_profiles">Profils de chat cachés</string>
<string name="v4_6_group_moderation_descr">Désormais, les administrateurs peuvent :
\n- supprimer les messages des membres.
\n- désactiver des membres (rôle \"observateur\")</string>
\n- désactiver des membres (rôle "observateur")</string>
<string name="save_welcome_message_question">Enregistrer le message d\'accueil?</string>
<string name="v4_6_group_welcome_message_descr">Choisissez un message à l\'attention des nouveaux membres !</string>
<string name="hide_profile">Masquer le profil</string>
@@ -822,8 +822,8 @@
<string name="theme_system">Sistema</string>
<string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string>
<string name="group_is_decentralized">Completamente decentralizzato: visibile solo ai membri.</string>
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string>
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string>
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica.</string>
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito.</string>
<string name="update_network_settings_confirmation">Aggiorna</string>
<string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string>
@@ -978,11 +978,11 @@
<string name="v4_6_chinese_spanish_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="hidden_profile_password">Password del profilo nascosta</string>
<string name="save_profile_password">Salva la password del profilo</string>
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\".</string>
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina "I tuoi profili di chat".</string>
<string name="password_to_show">Password per mostrare</string>
<string name="v4_6_group_moderation_descr">Ora gli amministratori possono:
\n- eliminare i messaggi dei membri.
\n- disattivare i membri (ruolo \"osservatore\")</string>
\n- disattivare i membri (ruolo "osservatore")</string>
<string name="hide_profile">Nascondi il profilo</string>
<string name="confirm_password">Conferma password</string>
<string name="error_updating_user_privacy">Errore nell\'aggiornamento della privacy dell\'utente</string>
@@ -1015,7 +1015,7 @@
<string name="group_invitation_tap_to_join">הקישו כדי להצטרף</string>
<string name="network_option_tcp_connection_timeout">תום זמן חיבור TCP</string>
<string name="periodic_notifications_desc">האפליקציה בודקת הודעות חדשות מעת לעת - היא משתמשת בכמה אחוזים מהסוללה ביום. האפליקציה לא משתמשת בהתראות דחיפה - נתונים מהמכשיר שלך לא נשלחים לשרתים.</string>
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־\"%s\". כל חברי הקבוצה יקבלו הודעה על כך.</string>
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־"%s". כל חברי הקבוצה יקבלו הודעה על כך.</string>
<string name="to_connect_via_link_title">כדי להתחבר באמצעות קישור</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">פלטפורמת ההודעות והיישומים המגנה על הפרטיות והאבטחה שלך.</string>
<string name="alert_text_msg_bad_id">המזהה של ההודעה הבאה שגוי (קטן או שווה להודעה הקודמת).
@@ -1027,7 +1027,7 @@
<string name="thank_you_for_installing_simplex">תודה שהתקנתם את SimpleX Chat!</string>
<string name="this_link_is_not_a_valid_connection_link">קישור זה אינו קישור חיבור תקין!</string>
<string name="theme_colors_section_title">צבעי ערכת נושא</string>
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־\"%s\". החבר יקבל הזמנה חדשה.</string>
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה.</string>
<string name="smp_servers_per_user">השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך</string>
<string name="first_platform_without_user_ids">הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה.</string>
<string name="next_generation_of_private_messaging">הדור הבא של תקשורת פרטית</string>
@@ -1836,7 +1836,7 @@
<string name="color_mode_system">מערכת</string>
<string name="error_showing_desktop_notification">שגיאה בהצגת התראה, צור קשר עם המפתחים</string>
<string name="smp_proxy_error_connecting">שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר</string>
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע\"י מנהל.</string>
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל.</string>
<string name="member_inactive_title">חבר לא פעיל</string>
<string name="xftp_servers_other">שרתי XFTP אחרים</string>
<string name="subscription_percentage">הראה אחוזים</string>
@@ -1920,11 +1920,11 @@
<string name="servers_info_files_tab">קבצים</string>
<string name="servers_info_missing">אין מידע, נסה לרענן</string>
<string name="servers_info">מידע על השרתים</string>
<string name="servers_info_detailed_statistics_received_total">התקבל סה\</string>
<string name="servers_info_detailed_statistics_received_total">התקבל סה"כ</string>
<string name="servers_info_detailed_statistics_receive_errors">התקבלו שגיאות</string>
<string name="reconnect">התחבר מחדש</string>
<string name="servers_info_detailed_statistics_sent_messages_header">שלח הודעות</string>
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה\</string>
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה"כ</string>
<string name="smp_server">שרת SMP</string>
<string name="xftp_server">שרת XFTP</string>
<string name="privacy_media_blur_radius_soft">חלש</string>
@@ -634,7 +634,7 @@
<string name="messages_section_title">메시지</string>
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
<string name="member_info_section_title_member">멤버</string>
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="member_role_will_be_changed_with_invitation">역할이 "%s"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
<string name="leave_group_button">나가기</string>
<string name="large_file">큰 파일!</string>
@@ -675,11 +675,11 @@
<string name="leave_group_question">그룹에서 나갈까요\?</string>
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="member_role_will_be_changed_with_notification">역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
<string name="notification_preview_mode_message">메시지 내용</string>
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 "삭제됨" 표시만 할 수 있음).</string>
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
@@ -714,7 +714,7 @@
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 "삭제됨"으로 표시만 할 수 있음).</string>
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
<string name="one_time_link">일회성 초대 링크</string>
<string name="paste_button">붙여넣기</string>
@@ -600,7 +600,7 @@
<string name="invite_prohibited">Nepavyko pakviesti kontakto!</string>
<string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string>
<string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string>
<string name="cannot_access_keychain">Negalima pasiekti "Keystore", kad išsaugotumėte duomenų bazės slaptažodį</string>
<string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string>
<string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string>
<string name="share_text_database_id">Duomenų bazės ID: %d</string>
@@ -1484,7 +1484,7 @@
<string name="no_info_on_delivery">Nėra pristatymo informacijos</string>
<string name="v4_6_group_moderation_descr">Dabar administratoriai gali:
\n- ištrinti narių žinutes.
\n- išjungti narius (\"stebėtojas\" rolė)</string>
\n- išjungti narius ("stebėtojas" rolė)</string>
<string name="chat_preferences_on">įj.</string>
<string name="unfavorite_chat">Nebemėgti</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių.</string>
@@ -280,7 +280,7 @@
<string name="custom_time_unit_weeks">ആഴ്ചകൾ</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">അനധികൃത അയക്കുക</string>
<string name="switch_receiving_address_question">സ്വീകരിക്കുന്ന വിലാസം മാറണോ\?</string>
<string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം \"%s\" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
<string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം "%s" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
<string name="la_lock_mode_system">സംവിധാനം പ്രാമാണീകരണം</string>
<string name="skip_inviting_button">അംഗങ്ങളെ ക്ഷണിക്കുന്നത് ഒഴിവാക്കുക</string>
<string name="save_welcome_message_question">സ്വാഗത സന്ദേശം സംരക്ഷിക്കണോ\?</string>
@@ -767,7 +767,7 @@
<string name="group_info_section_title_num_members">%1$s LEDEN</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert.</string>
<string name="switch_verb">Wijzig</string>
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht.</string>
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in "%s". Iedereen in de groep wordt op de hoogte gebracht.</string>
<string name="receiving_via">Ontvang via</string>
<string name="save_group_profile">Groep profiel opslaan</string>
<string name="group_is_decentralized">Volledig gedecentraliseerd alleen zichtbaar voor leden.</string>
@@ -800,7 +800,7 @@
<string name="button_remove_member">Lid verwijderen</string>
<string name="role_in_group">Rol</string>
<string name="button_send_direct_message">Direct bericht sturen</string>
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in \"%s\". De gebruiker ontvangt een nieuwe uitnodiging.</string>
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.</string>
<string name="sending_via">Verzenden via</string>
<string name="conn_stats_section_title_servers">SERVERS</string>
<string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string>
@@ -973,7 +973,7 @@
<string name="make_profile_private">Profiel privé maken!</string>
<string name="v4_6_group_moderation_descr">Nu kunnen beheerders:
\n- berichten van leden verwijderen.
\n- schakel leden uit (\"waarnemer\" rol)</string>
\n- schakel leden uit ("waarnemer" rol)</string>
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string>
<string name="password_to_show">Wachtwoord om weer te geven</string>
<string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string>
@@ -739,7 +739,7 @@
<string name="switch_verb">Przełącz</string>
<string name="switch_receiving_address">Zmień adres odbioru</string>
<string name="group_is_decentralized">W pełni zdecentralizowana widoczna tylko dla członków.</string>
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na \"%s\". Członek otrzyma nowe zaproszenie.</string>
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na "%s". Członek otrzyma nowe zaproszenie.</string>
<string name="group_welcome_title">Wiadomość powitalna</string>
<string name="users_delete_question">Usunąć profil czatu\?</string>
<string name="users_delete_profile_for">Usuń profil czatu dla</string>
@@ -944,7 +944,7 @@
<string name="failed_to_parse_chat_title">Nie udało się załadować czatu</string>
<string name="v4_6_group_moderation_descr">Teraz administratorzy mogą:
\n- usuwać wiadomości członków.
\n- wyłączyć członków (rola \"obserwatora\")</string>
\n- wyłączyć członków (rola "obserwatora")</string>
<string name="from_gallery_button">Z Galerii</string>
<string name="gallery_image_button">Obraz</string>
<string name="gallery_video_button">Wideo</string>
@@ -993,7 +993,7 @@
<string name="smp_servers_test_some_failed">Niektóre serwery nie przeszły testu:</string>
<string name="thank_you_for_installing_simplex">Dziękujemy za zainstalowanie SimpleX Chat!</string>
<string name="moderate_message_will_be_marked_warning">Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków.</string>
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni.</string>
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na "%s". Wszyscy w grupie zostaną powiadomieni.</string>
<string name="delete_files_and_media_desc">Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną.</string>
<string name="switch_receiving_address_desc">Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</string>
<string name="this_link_is_not_a_valid_connection_link">Ten link nie jest prawidłowym linkiem połączenia!</string>
@@ -695,14 +695,14 @@
<string name="chat_preferences_no">não</string>
<string name="v4_6_group_moderation_descr">Agora administradores podem:
\n- excluir mensagens de membros.
\n- desativar membros (cargo de \"observador\")</string>
\n- desativar membros (cargo de "observador")</string>
<string name="v4_6_group_moderation">Moderação do grupo</string>
<string name="v4_6_group_welcome_message">Mensagem de boas-vindas do grupo</string>
<string name="database_downgrade">Desatualizar banco de dados</string>
<string name="mtr_error_different">migração diferente no aplicativo/banco de dados: %s / %s</string>
<string name="invite_to_group_button">Convidar para o grupo</string>
<string name="no_contacts_to_add">Sem contatos para adicionar</string>
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para \"%s\". Todos no grupo serão notificados.</string>
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para "%s". Todos no grupo serão notificados.</string>
<string name="user_mute">Mutar</string>
<string name="only_you_can_send_voice">Somente você pode enviar mensagens de voz.</string>
<string name="only_your_contact_can_send_voice">Somente seu contato pode enviar mensagens de voz.</string>
@@ -813,7 +813,7 @@
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="snd_conn_event_switch_queue_phase_completed">você alterou o endereço</string>
<string name="database_upgrade">Atualização do banco de dados</string>
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para \"%s\". O membro receberá um novo convite.</string>
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para "%s". O membro receberá um novo convite.</string>
<string name="only_group_owners_can_change_prefs">Somente os proprietários do grupo podem alterar as preferências do grupo.</string>
<string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string>
<string name="group_welcome_title">Mensagem de boas-vindas</string>
@@ -631,7 +631,7 @@
<string name="chat_preferences_on">ligado</string>
<string name="v4_6_group_moderation_descr">Agora os administradores podem:
\n- eliminar mensagens de membros.
\n- desativar membros (função de \"observador\")</string>
\n- desativar membros (função de "observador")</string>
<string name="simplex_link_group">Ligação do grupo SimpleX</string>
<string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string>
<string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string>
@@ -796,8 +796,8 @@
<string name="change_verb">Поменять</string>
<string name="switch_verb">Переключить</string>
<string name="change_member_role_question">Поменять роль в группе?</string>
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на "%s". Все в группе получат сообщение.</string>
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на "%s". Будет отправлено новое приглашение.</string>
<string name="error_removing_member">Ошибка при удалении члена группы</string>
<string name="error_changing_role">Ошибка при изменении роли</string>
<string name="info_row_group">Группа</string>
@@ -964,7 +964,7 @@
<string name="v4_4_disappearing_messages">Исчезающие сообщения</string>
<string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string>
<string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string>
<string name="v4_4_live_messages">\"Живые\" сообщения</string>
<string name="v4_4_live_messages">"Живые" сообщения</string>
<string name="v4_4_live_messages_desc">Получатели видят их в то время как Вы их набираете.</string>
<string name="v4_4_verify_connection_security">Проверить безопасность соединения</string>
<string name="v4_4_verify_connection_security_desc">Сравните код безопасности с Вашими контактами.</string>
@@ -1065,7 +1065,7 @@
<string name="v4_6_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
<string name="v4_6_group_moderation_descr">Теперь админы могут:
\n- удалять сообщения членов.
\n- приостанавливать членов (роль \"наблюдатель\")</string>
\n- приостанавливать членов (роль "наблюдатель")</string>
<string name="v4_6_hidden_chat_profiles_descr">Защитите Ваши профили чата паролем!</string>
<string name="user_unhide">Раскрыть</string>
<string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string>
@@ -742,7 +742,7 @@
<string name="v4_6_reduced_battery_usage_descr">การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</string>
<string name="v4_6_group_moderation_descr">ขณะนี้ผู้ดูแลระบบสามารถ:
\n- ลบข้อความของสมาชิก
\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")</string>
\n- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</string>
<string name="v5_1_message_reactions">ปฏิกิริยาต่อข้อความ</string>
<string name="custom_time_unit_minutes">นาที</string>
<string name="custom_time_unit_months">เดือน</string>
@@ -1020,7 +1020,7 @@
<string name="v5_0_polish_interface_descr">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ</string>
<string name="next_generation_of_private_messaging">การส่งข้อความส่วนตัวรุ่นต่อไป</string>
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น "%s" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
<string name="delete_files_and_media_desc">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</string>
<string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string>
<string name="network_session_mode_transport_isolation">การแยกการขนส่ง</string>
@@ -1198,7 +1198,7 @@
<string name="button_welcome_message">ข้อความต้อนรับ</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</string>
<string name="you_can_share_this_address_with_your_contacts">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s</string>
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string>
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น "%s" สมาชิกจะได้รับคำเชิญใหม่</string>
<string name="group_welcome_title">ข้อความต้อนรับ</string>
<string name="group_main_profile_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</string>
<string name="update_network_settings_confirmation">อัปเดต</string>
@@ -138,14 +138,14 @@
<string name="save_and_notify_contact">Kaydet ve kişiyi bilgilendir</string>
<string name="save_passphrase_in_keychain">Parolayı, Keystore\'a kaydet.</string>
<string name="icon_descr_audio_on">Ses açık</string>
<string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
<string name="member_role_will_be_changed_with_notification">Yetki, "%s" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
<string name="calls_prohibited_with_this_contact">Sesli/görüntülü aramalar yasaktır.</string>
<string name="la_auth_failed">Kimlik doğrulama başarısız</string>
<string name="icon_descr_address">SimpleX Adresi</string>
<string name="moderate_message_will_be_deleted_warning">Mesaj, tüm üyeler için silinecek.</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu.</string>
<string name="settings_section_title_incognito">Gizlilik kipi</string>
<string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
<string name="member_role_will_be_changed_with_invitation">Yetki, "%s" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
<string name="role_in_group">Yetki</string>
<string name="allow_voice_messages_question">Sesli mesajlara izin verilsin mi?</string>
<string name="users_add">Profil ekle</string>
@@ -360,7 +360,7 @@
<string name="ttl_hours">%d saat</string>
<string name="v4_6_group_moderation_descr">Yeni yöneticiler artık:
\n- üyelerin mesajlarını silebilir.
\n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi verir)</string>
\n- üyeleri etkisizleştirebilir ("gözlemci" yetkisi verir)</string>
<string name="receipts_section_contacts">Konuşmalar</string>
<string name="create_group_link">Grup bağlantısı oluştur</string>
<string name="error_accepting_contact_request">Konuşma isteğini onaylarken hata oluştu</string>
@@ -726,8 +726,8 @@
<string name="share_text_disappears_at">Зникає о: %s</string>
<string name="item_info_current">(поточне)</string>
<string name="button_remove_member">Вилучити учасника</string>
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на \"%s\". Всі учасники групи будуть сповіщені.</string>
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на \"%s\". Учасник отримає нове запрошення.</string>
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на "%s". Всі учасники групи будуть сповіщені.</string>
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на "%s". Учасник отримає нове запрошення.</string>
<string name="info_row_group">Група</string>
<string name="group_welcome_title">Ласкаво просимо</string>
<string name="group_profile_is_stored_on_members_devices">Профіль групи зберігається на пристроях учасників, а не на серверах.</string>
@@ -1327,7 +1327,7 @@
<string name="enable_receipts_all">启用</string>
<string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string>
<string name="fix_connection_question">修复连接?</string>
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个"√"!✅</string>
<string name="setup_database_passphrase">设定数据库密码</string>
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
<string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string>
@@ -599,8 +599,8 @@
<string name="button_create_group_link">建立連結</string>
<string name="delete_link_question">刪除連結?</string>
<string name="button_remove_member">移除成員</string>
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 \"%s\"。所有在群組內的成員都接收到通知。</string>
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 \"%s\"。該成員將接收到新的邀請。</string>
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。</string>
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 "%s"。該成員將接收到新的邀請。</string>
<string name="network_status">網路狀態</string>
<string name="network_options_reset_to_defaults">重置為預設值</string>
<string name="incognito_info_share">當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。</string>
@@ -38,6 +38,41 @@
</description>
<releases>
<release version="6.0.4" date="2024-08-31">
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
<description>
<p>New in v6.0.1-4:</p>
<ul>
<li>reduce traffic and battery usage on unstable networks.</li>
<li>only offer the next versions for update, based on SerVer ordering.</li>
<li>UI fixes.</li>
<li>reduce app memory usage and start time.</li>
<li>faster sending of files to groups.</li>
<li>fix rare delivery bug.</li>
</ul>
<p>New in v6.0:</p>
<p>New chat experience:</p>
<ul>
<li>connect to your friends faster.</li>
<li>archive contacts to chat later.</li>
<li>delete up to 20 messages at once.</li>
<li>increase font size.</li>
</ul>
<p>New media options:</p>
<ul>
<li>play from the chat list.</li>
<li>blur for better privacy.</li>
</ul>
<p>Private routing:</p>
<ul>
<li>it protects your IP address and connections and is now enabled by default.</li>
</ul>
<p>Connection and servers information:</p>
<ul>
<li>to control your network status and usage.</li>
</ul>
</description>
</release>
<release version="6.0.3" date="2024-08-24">
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
<description>