mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 15:48:54 +00:00
Merge remote-tracking branch 'origin/master' into ab/wallet
This commit is contained in:
@@ -15,6 +15,7 @@ on:
|
||||
- "apps/simplex-chat/**"
|
||||
- "apps/simplex-bot/**"
|
||||
- "apps/simplex-bot-advanced/**"
|
||||
- "apps/simplex-badge-service/**"
|
||||
- "apps/simplex-broadcast-bot/**"
|
||||
- "apps/simplex-directory-service/**"
|
||||
- "tests/**"
|
||||
|
||||
@@ -56,6 +56,7 @@ website/src/images/
|
||||
website/src/js/lottie.min.js
|
||||
website/src/js/ethers.*
|
||||
website/src/js/directory.js
|
||||
website/src/js/page.js
|
||||
website/src/js/channel-preview.js
|
||||
website/src/js/simplex-lib.js
|
||||
website/src/file-assets/
|
||||
|
||||
@@ -220,6 +220,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent and important updates:
|
||||
|
||||
[Sep 19, 2026. SimpleX Supporter Badges — Send Larger Files That Stay Available Longer, Without Being Identified](./blog/20260919-simplex-supporter-badges.md)
|
||||
|
||||
[Aug 20, 2026. Equity Crowdfunding Launched - You Can Get a Stake in SimpleX Chat](./blog/20260819-simplex-chat-crowdfunding.md)
|
||||
|
||||
[Jul 22, 2026. SimpleX Public Names — a Name Nobody Can Take From You](./blog/20260722-simplex-public-names.md)
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"appPolicies" : {
|
||||
"eula" : "",
|
||||
"policies" : [
|
||||
{
|
||||
"locale" : "en_US",
|
||||
"policyText" : "",
|
||||
"policyURL" : ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"identifier" : "B4D6E1A0",
|
||||
"nonRenewingSubscriptions" : [
|
||||
|
||||
],
|
||||
"products" : [
|
||||
{
|
||||
"displayPrice" : "7.00",
|
||||
"familyShareable" : false,
|
||||
"internalID" : "3010000001",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Supporter badge for one month",
|
||||
"displayName" : "SimpleX Supporter Badge",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "BADGE_SUPPORTER_01",
|
||||
"referenceName" : "SimpleX Supporter Badge",
|
||||
"type" : "Consumable"
|
||||
},
|
||||
{
|
||||
"displayPrice" : "70.00",
|
||||
"familyShareable" : false,
|
||||
"internalID" : "3010000002",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Legend badge for one month",
|
||||
"displayName" : "SimpleX Legend Badge",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "BADGE_LEGEND_01",
|
||||
"referenceName" : "SimpleX Legend Badge",
|
||||
"type" : "Consumable"
|
||||
}
|
||||
],
|
||||
"settings" : {
|
||||
"_applicationInternalID" : "",
|
||||
"_developerTeamID" : "",
|
||||
"_failTransactionsEnabled" : false,
|
||||
"_lastSynchronizedDate" : 0,
|
||||
"_locale" : "en_US",
|
||||
"_storefront" : "USA",
|
||||
"_storeKitErrors" : [
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Load Products"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Purchase"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Verification"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Store Sync"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Subscription Status"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Transaction"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Manage Subscriptions Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Refund Request Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Offer Code Redeem Sheet"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subscriptionGroups" : [
|
||||
{
|
||||
"id" : "3020000001",
|
||||
"localizations" : [
|
||||
|
||||
],
|
||||
"name" : "SimpleX Badge Subscription",
|
||||
"subscriptions" : [
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "7.00",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 2,
|
||||
"internalID" : "3020000011",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Supporter badge, renews monthly",
|
||||
"displayName" : "SimpleX Supporter Badge (Month)",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "SUBSCR_BADGE_SUPPORTER_MONTH_01",
|
||||
"recurringSubscriptionPeriod" : "P1M",
|
||||
"referenceName" : "SimpleX Supporter Badge (Month)",
|
||||
"subscriptionGroupID" : "3020000001",
|
||||
"type" : "RecurringSubscription",
|
||||
"winbackOffers" : [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "42.00",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 2,
|
||||
"internalID" : "3020000012",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Supporter badge, renews yearly",
|
||||
"displayName" : "SimpleX Supporter Badge (Year)",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "SUBSCR_BADGE_SUPPORTER_YEAR_01",
|
||||
"recurringSubscriptionPeriod" : "P1Y",
|
||||
"referenceName" : "SimpleX Supporter Badge (Year)",
|
||||
"subscriptionGroupID" : "3020000001",
|
||||
"type" : "RecurringSubscription",
|
||||
"winbackOffers" : [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "70.00",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 1,
|
||||
"internalID" : "3020000013",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Legend badge, renews monthly",
|
||||
"displayName" : "SimpleX Legend Badge (Month)",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "SUBSCR_BADGE_LEGEND_MONTH_01",
|
||||
"recurringSubscriptionPeriod" : "P1M",
|
||||
"referenceName" : "SimpleX Legend Badge (Month)",
|
||||
"subscriptionGroupID" : "3020000001",
|
||||
"type" : "RecurringSubscription",
|
||||
"winbackOffers" : [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "420.00",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 1,
|
||||
"internalID" : "3020000014",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Legend badge, renews yearly",
|
||||
"displayName" : "SimpleX Legend Badge (Year)",
|
||||
"locale" : "en_US"
|
||||
}
|
||||
],
|
||||
"productID" : "SUBSCR_BADGE_LEGEND_YEAR_01",
|
||||
"recurringSubscriptionPeriod" : "P1Y",
|
||||
"referenceName" : "SimpleX Legend Badge (Year)",
|
||||
"subscriptionGroupID" : "3020000001",
|
||||
"type" : "RecurringSubscription",
|
||||
"winbackOffers" : [
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version" : {
|
||||
"major" : 4,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,7 @@ After completing all changes (code + documentation), you MUST run an adversarial
|
||||
| Shared/SimpleXApp.swift | spec/architecture.md | product/flows/onboarding.md |
|
||||
| Shared/AppDelegate.swift | spec/services/notifications.md | product/flows/onboarding.md |
|
||||
| Shared/Views/ChatList/ChatListView.swift | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| Shared/Views/ChatList/GetStakeBanner.swift | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| Shared/Views/Chat/ChatView.swift | spec/client/chat-view.md | product/views/chat.md |
|
||||
| Shared/Views/Chat/ComposeMessage/ComposeView.swift | spec/client/compose.md | product/views/chat.md |
|
||||
| Shared/Views/Chat/ChatItem/ | spec/client/chat-view.md | product/views/chat.md |
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wefunder_logo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -191,6 +191,11 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiUploadStandaloneFile(userId: Int64, file: CryptoFile)
|
||||
case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile)
|
||||
case apiStandaloneFileInfo(url: String)
|
||||
// badges
|
||||
case apiRedeemBadgeCode(userId: Int64, code: String)
|
||||
case apiGetBadgeState(userId: Int64)
|
||||
case apiGetBadgeLedger(userId: Int64, badgePurchaseId: Int64)
|
||||
case apiAckBadgeAlert(userId: Int64, badgePurchaseId: Int64, alertKind: BadgeAlertKind, snooze: Bool, episode: String)
|
||||
// misc
|
||||
case showVersion
|
||||
case getAgentSubsTotal(userId: Int64)
|
||||
@@ -413,6 +418,11 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiUploadStandaloneFile(userId, file): return "/_upload \(userId) \(file.filePath)"
|
||||
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
||||
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
||||
case let .apiRedeemBadgeCode(userId, code): return "/_redeem_badge_code \(userId) \(code)"
|
||||
case let .apiGetBadgeState(userId): return "/_badge state \(userId)"
|
||||
case let .apiGetBadgeLedger(userId, badgePurchaseId): return "/_badge ledger \(userId) \(badgePurchaseId)"
|
||||
case let .apiAckBadgeAlert(userId, badgePurchaseId, alertKind, snooze, episode):
|
||||
return "/_badge ack \(userId) \(badgePurchaseId) \(badgeAlertKindParam(alertKind)) \(onOff(snooze)) \(episode)"
|
||||
case .showVersion: return "/version"
|
||||
case let .getAgentSubsTotal(userId): return "/get subs total \(userId)"
|
||||
case let .getAgentServersSummary(userId): return "/get servers summary \(userId)"
|
||||
@@ -600,6 +610,10 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiUploadStandaloneFile: return "apiUploadStandaloneFile"
|
||||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||
case .apiRedeemBadgeCode: return "apiRedeemBadgeCode"
|
||||
case .apiGetBadgeState: return "apiGetBadgeState"
|
||||
case .apiGetBadgeLedger: return "apiGetBadgeLedger"
|
||||
case .apiAckBadgeAlert: return "apiAckBadgeAlert"
|
||||
case .showVersion: return "showVersion"
|
||||
case .getAgentSubsTotal: return "getAgentSubsTotal"
|
||||
case .getAgentServersSummary: return "getAgentServersSummary"
|
||||
@@ -653,6 +667,9 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
|
||||
case let .testStorageEncryption(key):
|
||||
return .testStorageEncryption(key: obfuscate(key))
|
||||
// a code is a bearer secret until it is redeemed, and the terminal shows and copies cmdString
|
||||
case let .apiRedeemBadgeCode(userId, code):
|
||||
return .apiRedeemBadgeCode(userId: userId, code: obfuscate(code))
|
||||
default: return self
|
||||
}
|
||||
}
|
||||
@@ -679,6 +696,17 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
|
||||
}
|
||||
|
||||
// /_badge ack takes the kind in core's text encoding, not the JSON tag
|
||||
private func badgeAlertKindParam(_ kind: BadgeAlertKind) -> String {
|
||||
switch kind {
|
||||
case .renewalApproaching: "renewal_approaching"
|
||||
case .paymentIssue: "payment_issue"
|
||||
case .subscriptionEnded: "subscription_ended"
|
||||
case .prepaidEnding: "prepaid_ending"
|
||||
case .supportEnded: "support_ended"
|
||||
}
|
||||
}
|
||||
|
||||
private func maybeContent(_ mc: MsgContent?) -> String {
|
||||
if case let .text(s) = mc, s.isEmpty {
|
||||
""
|
||||
@@ -1019,6 +1047,11 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case archiveExported(archiveErrors: [ArchiveError])
|
||||
case archiveImported(archiveErrors: [ArchiveError])
|
||||
case appSettings(appSettings: AppSettings)
|
||||
// badges
|
||||
// the full user, not UserRef: its profile carries the badge that setUserBadge just stored
|
||||
case badgeRedeemed(user: User, redeemedBadge: LocalBadge, newBadge: Bool, badgeState: BadgeState?)
|
||||
case badgeState(user: UserRef, badgeState: BadgeState?)
|
||||
case badgeLedger(user: UserRef, badgeLedger: [StatementEntry])
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
@@ -1070,6 +1103,9 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case .archiveExported: "archiveExported"
|
||||
case .archiveImported: "archiveImported"
|
||||
case .appSettings: "appSettings"
|
||||
case .badgeRedeemed: "badgeRedeemed"
|
||||
case .badgeState: "badgeState"
|
||||
case .badgeLedger: "badgeLedger"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,6 +1159,9 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case let .archiveExported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||
case let .badgeRedeemed(u, redeemedBadge, newBadge, badgeState): return withUser(u, "redeemedBadge: \(String(describing: redeemedBadge))\nnewBadge: \(newBadge)\nbadgeState: \(String(describing: badgeState))")
|
||||
case let .badgeState(u, badgeState): return withUser(u, String(describing: badgeState))
|
||||
case let .badgeLedger(u, badgeLedger): return withUser(u, String(describing: badgeLedger))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1205,6 +1244,9 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
||||
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||
// pq
|
||||
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
||||
// badges
|
||||
case badgeChanged(user: User, badgeState: BadgeState?)
|
||||
case badgeAlert(user: UserRef, badgeAlert: BadgeAlert)
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
@@ -1277,6 +1319,8 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
||||
case .remoteCtrlConnected: "remoteCtrlConnected"
|
||||
case .remoteCtrlStopped: "remoteCtrlStopped"
|
||||
case .contactPQEnabled: "contactPQEnabled"
|
||||
case .badgeChanged: "badgeChanged"
|
||||
case .badgeAlert: "badgeAlert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,6 +1403,8 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))"
|
||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
||||
case let .badgeChanged(u, badgeState): return withUser(u, String(describing: badgeState))
|
||||
case let .badgeAlert(u, badgeAlert): return withUser(u, String(describing: badgeAlert))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +359,30 @@ class ChannelRelaysModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// The badge of whichever profile it was last loaded for, kept current by the badgeChanged event so
|
||||
// that a screen already open shows what the renewal worker did with no command behind it.
|
||||
class BadgeModel: ObservableObject {
|
||||
static let shared = BadgeModel()
|
||||
@Published private(set) var userId: Int64?
|
||||
@Published private(set) var badgeState: BadgeState?
|
||||
@Published private(set) var alert: BadgeAlert?
|
||||
|
||||
// alert follows the state: getUserBadgeState derives it on every read, so a badgeChanged is
|
||||
// never staler than the alert it carries - the invariant a new alert kind must keep
|
||||
func set(userId: Int64, badgeState: BadgeState?) {
|
||||
self.userId = userId
|
||||
self.badgeState = badgeState
|
||||
alert = badgeState?.alert
|
||||
}
|
||||
|
||||
func setAlert(userId: Int64, alert: BadgeAlert) {
|
||||
if self.userId == userId {
|
||||
self.alert = alert
|
||||
badgeState?.alert = alert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: spec/state.md#ChatModel
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
|
||||
@@ -124,8 +124,8 @@ func chatSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDe
|
||||
}
|
||||
|
||||
// Spec: spec/api.md#chatApiSendCmdWithRetry
|
||||
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue<Bool>? = nil, retryNum: Int32 = 0) async -> APIResult<R>? {
|
||||
let r: APIResult<R> = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum)
|
||||
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue<Bool>? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult<R>? {
|
||||
let r: APIResult<R> = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum, log: log)
|
||||
if inProgress == nil || inProgress?.boxedValue == true,
|
||||
case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) {
|
||||
return await withCheckedContinuation { cont in
|
||||
@@ -135,7 +135,7 @@ func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool
|
||||
cont.resume(returning: nil)
|
||||
},
|
||||
onRetry: {
|
||||
let r1: APIResult<R>? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, inProgress: inProgress, retryNum: retryNum + 1)
|
||||
let r1: APIResult<R>? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, inProgress: inProgress, retryNum: retryNum + 1, log: log)
|
||||
cont.resume(returning: r1)
|
||||
}
|
||||
)
|
||||
@@ -200,6 +200,10 @@ func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: Stri
|
||||
title: NSLocalizedString("No private routing session", comment: "alert title"),
|
||||
message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer)
|
||||
)
|
||||
case .errorAgent(.AGENT(.A_SERVICE(.timeout))): (
|
||||
title: NSLocalizedString("Connection timeout", comment: "alert title"),
|
||||
message: NSLocalizedString("The service did not respond. Please try again.", comment: "alert message")
|
||||
)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -2182,6 +2186,74 @@ func resetAgentServersStats() async throws {
|
||||
try await sendCommandOkResp(.resetAgentServersStats)
|
||||
}
|
||||
|
||||
// log: false because the code is a bearer secret until it is redeemed - it is in the command.
|
||||
// nil when the user cancels the retry alert.
|
||||
func apiRedeemBadgeCode(_ userId: Int64, _ code: String) async throws -> (user: User, newBadge: Bool, badgeState: BadgeState?)? {
|
||||
let r: APIResult<ChatResponse2>? = await chatApiSendCmdWithRetry(.apiRedeemBadgeCode(userId: userId, code: code), log: false)
|
||||
guard let r else { return nil }
|
||||
// redeemedBadge is dropped: the user's profile carries what is shown
|
||||
if case let .result(.badgeRedeemed(user, _, newBadge, badgeState)) = r { return (user, newBadge, badgeState) }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
// localized where the user can act on it; otherwise the error itself, so a screenshot says what happened
|
||||
func redeemErrorText(_ error: Error) -> String {
|
||||
if case let .error(.badgeRedeemError(e)) = error as? ChatError {
|
||||
switch e {
|
||||
case .invalidCode: return NSLocalizedString("This code is not valid.", comment: "alert message")
|
||||
case .serviceNotConfigured: return NSLocalizedString("This app version cannot redeem badge codes.", comment: "alert message")
|
||||
case .badgeActive: return NSLocalizedString("This profile already has a badge. Redeem the code on another profile, or once this badge ends.", comment: "alert message")
|
||||
case .serviceError(.codeInvalid): return NSLocalizedString("This code was not recognised.", comment: "alert message")
|
||||
case .serviceError(.codeUsed): return NSLocalizedString("This code has already been used.", comment: "alert message")
|
||||
case .serviceError(.codeExpired): return NSLocalizedString("This code has expired.", comment: "alert message")
|
||||
case .serviceError(.rateLimited): return NSLocalizedString("Too many attempts. Please try again later.", comment: "alert message")
|
||||
case .serviceError(.unsupportedVersion): return NSLocalizedString("This app version is too old for the badge service. Please update the app.", comment: "alert message")
|
||||
case .serviceError: break
|
||||
case let .invalidResponse(message):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("The badge service sent an unexpected response: %@", comment: "alert message"), message)
|
||||
case .unknownKeyIndex, .credentialNotVerified: return NSLocalizedString("This app version cannot verify this badge. Please update the app.", comment: "alert message")
|
||||
}
|
||||
}
|
||||
return String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "alert message"), responseError(error))
|
||||
}
|
||||
|
||||
func apiGetBadgeState(_ userId: Int64) async throws -> BadgeState? {
|
||||
let r: ChatResponse2 = try await chatSendCmd(.apiGetBadgeState(userId: userId))
|
||||
if case let .badgeState(_, badgeState) = r { return badgeState }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiGetBadgeStateSync(_ userId: Int64) throws -> BadgeState? {
|
||||
let r: ChatResponse2 = try chatSendCmdSync(.apiGetBadgeState(userId: userId))
|
||||
if case let .badgeState(_, badgeState) = r { return badgeState }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiGetBadgeLedger(_ userId: Int64, _ badgePurchaseId: Int64) async throws -> [StatementEntry] {
|
||||
let r: ChatResponse2 = try await chatSendCmd(.apiGetBadgeLedger(userId: userId, badgePurchaseId: badgePurchaseId))
|
||||
if case let .badgeLedger(_, badgeLedger) = r { return badgeLedger }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiAckBadgeAlert(_ userId: Int64, _ badgePurchaseId: Int64, _ alertKind: BadgeAlertKind, snooze: Bool, episode: String) async throws -> BadgeState? {
|
||||
let r: ChatResponse2 = try await chatSendCmd(.apiAckBadgeAlert(userId: userId, badgePurchaseId: badgePurchaseId, alertKind: alertKind, snooze: snooze, episode: episode))
|
||||
if case let .badgeState(_, badgeState) = r { return badgeState }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
// An API call and not a stored flag: the ack is kept on the purchase in core, which then stops
|
||||
// raising this occurrence on every pass and across restarts, or until a snooze lapses.
|
||||
func ackBadgeAlert(snooze: Bool) async {
|
||||
let badgeModel = BadgeModel.shared
|
||||
guard let userId = badgeModel.userId, let purchaseId = badgeModel.badgeState?.badgePurchaseId, let alert = badgeModel.alert else { return }
|
||||
do {
|
||||
let badgeState = try await apiAckBadgeAlert(userId, purchaseId, alert.kind, snooze: snooze, episode: alert.episode)
|
||||
await MainActor.run { badgeModel.set(userId: userId, badgeState: badgeState) }
|
||||
} catch let error {
|
||||
logger.error("ackBadgeAlert: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||
if let userId = ChatModel.shared.currentUser?.userId {
|
||||
return userId
|
||||
@@ -2352,12 +2424,34 @@ func getUserChatData() throws {
|
||||
tm.activeFilter = nil
|
||||
tm.userTags = tags
|
||||
tm.updateChatTags(m.chats)
|
||||
loadBadgeState()
|
||||
}
|
||||
|
||||
// Not thrown: a failed badge read must not stop the app starting, and the model is left alone
|
||||
// rather than set to nil, which would read as "no badge".
|
||||
private func loadBadgeState() {
|
||||
do {
|
||||
let userId = try currentUserId("loadBadgeState")
|
||||
let badgeState = try apiGetBadgeStateSync(userId)
|
||||
BadgeModel.shared.set(userId: userId, badgeState: badgeState)
|
||||
} catch let error {
|
||||
logger.error("loadBadgeState: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadBadgeStateAsync(_ userId: Int64) async {
|
||||
do {
|
||||
let badgeState = try await apiGetBadgeState(userId)
|
||||
await MainActor.run { BadgeModel.shared.set(userId: userId, badgeState: badgeState) }
|
||||
} catch let error {
|
||||
logger.error("loadBadgeState: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync(keepingChatId: String?) async throws {
|
||||
let m = ChatModel.shared
|
||||
let tm = ChatTagsModel.shared
|
||||
if m.currentUser != nil {
|
||||
if let userId = m.currentUser?.userId {
|
||||
let userAddress = try await apiGetUserAddressAsync()
|
||||
let chatItemTTL = try await getChatItemTTLAsync()
|
||||
let chats = try await apiGetChatsAsync()
|
||||
@@ -2370,6 +2464,7 @@ private func getUserChatDataAsync(keepingChatId: String?) async throws {
|
||||
tm.userTags = tags
|
||||
tm.updateChatTags(m.chats)
|
||||
}
|
||||
await loadBadgeStateAsync(userId)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
m.userAddress = nil
|
||||
@@ -2948,6 +3043,20 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .badgeChanged(user, badgeState):
|
||||
await MainActor.run {
|
||||
// read by core after retiring or presenting, so it carries the profile badge as changed
|
||||
m.updateUser(user)
|
||||
if active(user) {
|
||||
BadgeModel.shared.set(userId: user.userId, badgeState: badgeState)
|
||||
}
|
||||
}
|
||||
case let .badgeAlert(user, badgeAlert):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
BadgeModel.shared.setAlert(userId: user.userId, alert: badgeAlert)
|
||||
}
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// BadgeStore.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 14.08.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import StoreKit
|
||||
|
||||
// TODO [badges] product ids will come from app config and prices from the badge service catalog;
|
||||
// hardcoded here so the App Store integration can be tested before the purchase API lands.
|
||||
func badgeProductId(_ level: BadgeLevel, _ period: BadgePeriod) -> String {
|
||||
switch (level, period) {
|
||||
case (.supporter, .oneMonth): "BADGE_SUPPORTER_01"
|
||||
case (.supporter, .monthly): "SUBSCR_BADGE_SUPPORTER_MONTH_01"
|
||||
case (.supporter, .annual): "SUBSCR_BADGE_SUPPORTER_YEAR_01"
|
||||
case (.legend, .oneMonth): "BADGE_LEGEND_01"
|
||||
case (.legend, .monthly): "SUBSCR_BADGE_LEGEND_MONTH_01"
|
||||
case (.legend, .annual): "SUBSCR_BADGE_LEGEND_YEAR_01"
|
||||
}
|
||||
}
|
||||
|
||||
let badgeProductIds: [String] = BadgeLevel.allCases.flatMap { level in
|
||||
BadgePeriod.allCases.map { badgeProductId(level, $0) }
|
||||
}
|
||||
|
||||
// TODO [badges] replaced by APIGetBadgeInvoice, which creates the invoice row and returns its id.
|
||||
// Apple requires a UUID - it is sent as appAccountToken and echoed back in the signed transaction,
|
||||
// which is how the service learns which invoice a store transaction settles.
|
||||
func newBadgeInvoiceId() -> UUID { UUID() }
|
||||
|
||||
enum BadgePrice {
|
||||
case loading
|
||||
case price(String)
|
||||
case unavailable
|
||||
|
||||
var canPurchase: Bool {
|
||||
switch self {
|
||||
case .price: true
|
||||
case .loading, .unavailable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgeStoreReceipt {
|
||||
// the signed token the badge service verifies - never transaction.jsonRepresentation
|
||||
let jws: String
|
||||
let productId: String
|
||||
let transactionId: UInt64
|
||||
let invoiceId: UUID?
|
||||
let environment: String?
|
||||
let signatureVerified: Bool
|
||||
}
|
||||
|
||||
enum BadgePurchaseOutcome {
|
||||
case purchased(BadgeStoreReceipt)
|
||||
case pending
|
||||
case cancelled
|
||||
}
|
||||
|
||||
enum BadgeStoreError: Error {
|
||||
case productUnavailable(productId: String)
|
||||
case unknownPurchaseResult
|
||||
}
|
||||
|
||||
final class BadgeStore: ObservableObject {
|
||||
static let shared = BadgeStore()
|
||||
|
||||
private enum LoadState { case notLoaded, loading, loaded, failed }
|
||||
|
||||
@Published private var state: LoadState = .notLoaded
|
||||
private var products: [String: Product] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
func price(_ level: BadgeLevel, _ period: BadgePeriod) -> BadgePrice {
|
||||
switch state {
|
||||
case .notLoaded, .loading: return .loading
|
||||
case .loaded, .failed:
|
||||
if let p = products[badgeProductId(level, period)] { return .price(compactPrice(p)) }
|
||||
return .unavailable
|
||||
}
|
||||
}
|
||||
|
||||
// percentage the annual subscription saves against 12 monthly payments
|
||||
func annualSavings(_ level: BadgeLevel) -> Int? {
|
||||
guard let monthly = products[badgeProductId(level, .monthly)],
|
||||
let annual = products[badgeProductId(level, .annual)]
|
||||
else { return nil }
|
||||
let year = monthly.price * 12
|
||||
guard year > 0, annual.price < year else { return nil }
|
||||
let saved = (year - annual.price) / year * 100
|
||||
let percent = Int(NSDecimalNumber(decimal: saved).doubleValue.rounded())
|
||||
return percent > 0 ? percent : nil
|
||||
}
|
||||
|
||||
func load() async {
|
||||
guard await startLoading() else { return }
|
||||
do {
|
||||
let loaded = try await Product.products(for: badgeProductIds)
|
||||
let byId = Dictionary(loaded.map { ($0.id, $0) }, uniquingKeysWith: { p, _ in p })
|
||||
let missing = badgeProductIds.filter { byId[$0] == nil }
|
||||
if !missing.isEmpty {
|
||||
// the store drops ids it cannot resolve without saying why, so the storefront and
|
||||
// the bundle the request was made for are logged with them
|
||||
let storefront = await Storefront.current
|
||||
logger.warning("BadgeStore.load: no product returned for \(missing.joined(separator: ", ")) - bundle \(Bundle.main.bundleIdentifier ?? "?"), storefront \(storefront?.countryCode ?? "none"), canMakePayments \(AppStore.canMakePayments)")
|
||||
}
|
||||
await MainActor.run {
|
||||
products = byId
|
||||
state = .loaded
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("BadgeStore.load: \(String(describing: error))")
|
||||
await MainActor.run { state = .failed }
|
||||
}
|
||||
}
|
||||
|
||||
func purchase(_ level: BadgeLevel, _ period: BadgePeriod, invoiceId: UUID) async throws -> BadgePurchaseOutcome {
|
||||
let productId = badgeProductId(level, period)
|
||||
guard let product = await MainActor.run(body: { products[productId] }) else {
|
||||
throw BadgeStoreError.productUnavailable(productId: productId)
|
||||
}
|
||||
switch try await product.purchase(options: [.appAccountToken(invoiceId)]) {
|
||||
case let .success(verification):
|
||||
let transaction: Transaction
|
||||
let signatureVerified: Bool
|
||||
switch verification {
|
||||
case let .verified(t):
|
||||
transaction = t
|
||||
signatureVerified = true
|
||||
case let .unverified(t, _):
|
||||
transaction = t
|
||||
signatureVerified = false
|
||||
}
|
||||
// nothing is delivered in this build, so the transaction is finished right away; once the
|
||||
// service issues credentials it must only be finished after the credential is stored
|
||||
await transaction.finish()
|
||||
return .purchased(storeReceipt(verification.jwsRepresentation, transaction, signatureVerified))
|
||||
case .pending: return .pending
|
||||
case .userCancelled: return .cancelled
|
||||
@unknown default: throw BadgeStoreError.unknownPurchaseResult
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func startLoading() -> Bool {
|
||||
switch state {
|
||||
case .notLoaded, .failed:
|
||||
state = .loading
|
||||
return true
|
||||
case .loading, .loaded:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drops the fraction from whole amounts ("$7", not "$7.00") in the product's own currency style;
|
||||
// Product.displayPrice remains the exact form for views that need the cents
|
||||
private func compactPrice(_ product: Product) -> String {
|
||||
var whole = Decimal()
|
||||
var price = product.price
|
||||
NSDecimalRound(&whole, &price, 0, .plain)
|
||||
return whole == product.price
|
||||
? product.price.formatted(product.priceFormatStyle.precision(.fractionLength(0)))
|
||||
: product.displayPrice
|
||||
}
|
||||
|
||||
private func storeReceipt(_ jws: String, _ t: Transaction, _ signatureVerified: Bool) -> BadgeStoreReceipt {
|
||||
var environment: String? = nil
|
||||
if #available(iOS 16.0, *) { environment = t.environment.rawValue }
|
||||
return BadgeStoreReceipt(
|
||||
jws: jws,
|
||||
productId: t.productID,
|
||||
transactionId: t.id,
|
||||
invoiceId: t.appAccountToken,
|
||||
environment: environment,
|
||||
signatureVerified: signatureVerified
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// BadgeUserPreview.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 30.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgeUserPreview<Trailing: View>: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
let level: BadgeLevel
|
||||
let trailing: () -> Trailing
|
||||
|
||||
init(level: BadgeLevel, @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() }) {
|
||||
self.level = level
|
||||
self.trailing = trailing
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let user = chatModel.currentUser
|
||||
let displayName = user?.displayName ?? NSLocalizedString("My nickname", comment: "badges preview placeholder")
|
||||
let previewBadge = LocalBadge(
|
||||
// fabricated for the preview: the status is given here, and NameBadge renders from it alone
|
||||
badge: BadgeInfo(badgeType: level.badgeType, badgeExpiry: .distantFuture),
|
||||
status: .active
|
||||
)
|
||||
return VStack(spacing: 12) {
|
||||
ProfileImage(imageStr: user?.image, size: 128)
|
||||
HStack(alignment: .center, spacing: 6) {
|
||||
NameWithBadge(Text(displayName).font(.largeTitle), previewBadge, .largeTitle)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
trailing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// BadgesHowItWorksView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgesHowItWorksView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("How badges protect your privacy")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("A badge is an anonymous credential stored in your profile on your device. This credential is never sent to anyone.")
|
||||
Text("To prove to a contact or a server that you have a badge, the app generates a new proof that reveals only the badge type and the expiry date, rounded to a week.")
|
||||
Text("Nobody can link two different proofs to each other or to the purchase.")
|
||||
ExternalLink("Read more in our blog.", destination: URL(string: "https://simplex.chat/blog/20260919-simplex-supporter-badges.html")!)
|
||||
}
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesHowItWorksView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesHowItWorksView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// BadgesLedgerView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 17.09.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgesLedgerView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
let badgeState: BadgeState
|
||||
@State private var entries: [StatementEntry]? = nil
|
||||
@State private var expanded: Set<String> = []
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if let entries {
|
||||
Section {
|
||||
if entries.isEmpty {
|
||||
Text("No entries")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
ForEach(entries, id: \.entryId) { entry in
|
||||
ledgerRow(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Badge ledger")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button { showShareSheet(items: [ledgerShareText()]) } label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.disabled(entries?.isEmpty ?? true)
|
||||
}
|
||||
}
|
||||
.onAppear(perform: loadLedger)
|
||||
}
|
||||
|
||||
@ViewBuilder private func ledgerRow(_ entry: StatementEntry) -> some View {
|
||||
let isExpanded = expanded.contains(entry.entryId)
|
||||
Button {
|
||||
withAnimation {
|
||||
if isExpanded { expanded.remove(entry.entryId) } else { expanded.insert(entry.entryId) }
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.entryType.text)
|
||||
Text(dateText(entry.createdAt))
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Text(changeText(entry))
|
||||
.foregroundStyle(.secondary)
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
if isExpanded {
|
||||
ForEach(entryFields(entry), id: \.0) { field in
|
||||
infoRow(Text(field.0), field.1).padding(.leading, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func changeText(_ entry: StatementEntry) -> String {
|
||||
let n = entry.changeMonths
|
||||
let months = abs(n) == 1
|
||||
? String.localizedStringWithFormat(NSLocalizedString("%d month", comment: "time interval"), n)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("%d months", comment: "time interval"), n)
|
||||
return n > 0 ? "+" + months : months
|
||||
}
|
||||
|
||||
private func entryFields(_ entry: StatementEntry) -> [(String, String)] {
|
||||
var fields = [
|
||||
(NSLocalizedString("Date", comment: "ledger entry field"), dateTimeText(entry.createdAt)),
|
||||
(NSLocalizedString("Balance", comment: "ledger entry field"), "\(entry.balanceMonths)"),
|
||||
(NSLocalizedString("Balance start", comment: "ledger entry field"), dateTimeText(entry.balanceStartTs)),
|
||||
(NSLocalizedString("Anchor", comment: "ledger entry field"), dateTimeText(entry.balanceAnchorTs)),
|
||||
(NSLocalizedString("Badge type", comment: "ledger entry field"), entry.balanceBadgeType.text)
|
||||
]
|
||||
if let pausedSince = entry.wasPausedSince {
|
||||
fields.append((NSLocalizedString("Paused since", comment: "ledger entry field"), dateTimeText(pausedSince)))
|
||||
}
|
||||
fields.append((NSLocalizedString("Entry ID", comment: "ledger entry field"), entry.entryId))
|
||||
if let payload = payloadField(entry.entryType) {
|
||||
fields.append(payload)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
private func payloadField(_ entryType: StatementEntryType) -> (String, String)? {
|
||||
switch entryType {
|
||||
case let .credit(credit):
|
||||
switch credit {
|
||||
case let .payment(invoiceId):
|
||||
return invoiceId.map { (NSLocalizedString("Invoice ID", comment: "ledger entry field"), $0) }
|
||||
case let .charge(chargeId):
|
||||
return (NSLocalizedString("Charge ID", comment: "ledger entry field"), chargeId)
|
||||
case let .transferIn(fromPurchaseKey):
|
||||
return (NSLocalizedString("From purchase key", comment: "ledger entry field"), fromPurchaseKey)
|
||||
case .code, .support, .opening, .unknown:
|
||||
return nil
|
||||
}
|
||||
case let .debit(debit):
|
||||
switch debit {
|
||||
case let .upgrade(toPurchaseKey), let .transferOut(toPurchaseKey):
|
||||
return (NSLocalizedString("To purchase key", comment: "ledger entry field"), toPurchaseKey)
|
||||
case .refund, .support, .badge, .lapse, .unknown:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the JSON as core sent it: English field names and ISO dates, for support
|
||||
private func ledgerShareText() -> String {
|
||||
let encoder = getJSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = (try? encoder.encode(entries ?? [])) ?? Data()
|
||||
return String(decoding: data, as: UTF8.self)
|
||||
}
|
||||
|
||||
private func dateText(_ date: Date) -> String {
|
||||
DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .none)
|
||||
}
|
||||
|
||||
private func dateTimeText(_ date: Date) -> String {
|
||||
DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .short)
|
||||
}
|
||||
|
||||
private func loadLedger() {
|
||||
guard let user = chatModel.currentUser else { return }
|
||||
Task {
|
||||
do {
|
||||
let ledger = try await apiGetBadgeLedger(user.userId, badgeState.badgePurchaseId)
|
||||
await MainActor.run { entries = ledger }
|
||||
} catch let e {
|
||||
logger.error("apiGetBadgeLedger error: \(responseError(e))")
|
||||
await MainActor.run {
|
||||
showErrorAlert(e, NSLocalizedString("Error", comment: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//
|
||||
// BadgesPayView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum BadgePeriod: String, CaseIterable, Identifiable {
|
||||
case oneMonth
|
||||
case monthly
|
||||
case annual
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .oneMonth: "calendar"
|
||||
case .monthly: "arrow.clockwise"
|
||||
case .annual: "arrow.clockwise"
|
||||
}
|
||||
}
|
||||
|
||||
var label: LocalizedStringKey {
|
||||
switch self {
|
||||
case .oneMonth: "1 month"
|
||||
case .monthly: "Monthly"
|
||||
case .annual: "Annual"
|
||||
}
|
||||
}
|
||||
|
||||
func priceText(_ price: BadgePrice) -> Text {
|
||||
switch price {
|
||||
case .loading: return Text(verbatim: "…")
|
||||
case .unavailable: return Text(verbatim: "—")
|
||||
case let .price(p):
|
||||
switch self {
|
||||
case .oneMonth: return Text(verbatim: p)
|
||||
case .monthly: return Text("\(p)/month")
|
||||
case .annual: return Text("\(p)/year")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func payText(_ price: BadgePrice) -> Text {
|
||||
switch price {
|
||||
case .loading: return Text("Loading…")
|
||||
case .unavailable: return Text("Not available")
|
||||
case let .price(p):
|
||||
switch self {
|
||||
case .oneMonth: return Text("Pay \(p)")
|
||||
case .monthly: return Text("Pay \(p)/month")
|
||||
case .annual: return Text("Pay \(p)/year")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesPayView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject private var store = BadgeStore.shared
|
||||
let level: BadgeLevel
|
||||
@State private var selectedPeriod: BadgePeriod = .monthly
|
||||
@State private var purchasing = false
|
||||
// presented from this view, not AlertManager: its host is behind the sheet these views open in
|
||||
@State private var alert: SomeAlert?
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text(level.title)
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
BadgeUserPreview(level: level)
|
||||
.padding(.top, 4)
|
||||
|
||||
Text(level.tagline)
|
||||
.font(.body)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 4)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
// fixedSize + maxHeight on the cards so all three match the tallest one -
|
||||
// only Annual carries a savings line, and prices wrap at large fonts
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
periodCard(.oneMonth)
|
||||
periodCard(.monthly)
|
||||
periodCard(.annual)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
payButton()
|
||||
.padding(.vertical, 10)
|
||||
Text(billingFooter)
|
||||
.font(.footnote)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(height: 22)
|
||||
}
|
||||
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 0)
|
||||
.padding(.bottom, 20)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task { await store.load() }
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
private func periodCard(_ period: BadgePeriod) -> some View {
|
||||
let isSelected = period == selectedPeriod
|
||||
return Button {
|
||||
selectedPeriod = period
|
||||
} label: {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: period.icon)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(isSelected ? theme.colors.primary : theme.colors.secondary)
|
||||
Text(period.label)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
period.priceText(store.price(level, period))
|
||||
.font(.body)
|
||||
if let percent = savingsPercent(period) {
|
||||
Text("Save \(percent)%")
|
||||
.font(.footnote)
|
||||
.foregroundColor(isSelected ? theme.colors.primary : theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.vertical, 25)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(isSelected ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func savingsPercent(_ period: BadgePeriod) -> Int? {
|
||||
period == .annual ? store.annualSavings(level) : nil
|
||||
}
|
||||
|
||||
private func payButton() -> some View {
|
||||
let price = store.price(level, selectedPeriod)
|
||||
let disabled = !price.canPurchase || purchasing
|
||||
return Button {
|
||||
purchase()
|
||||
} label: {
|
||||
selectedPeriod.payText(price)
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: disabled))
|
||||
.disabled(disabled)
|
||||
}
|
||||
|
||||
private func purchase() {
|
||||
let period = selectedPeriod
|
||||
let invoiceId = newBadgeInvoiceId()
|
||||
purchasing = true
|
||||
Task {
|
||||
do {
|
||||
let outcome = try await store.purchase(level, period, invoiceId: invoiceId)
|
||||
await MainActor.run {
|
||||
purchasing = false
|
||||
switch outcome {
|
||||
case let .purchased(receipt): showPurchasedAlert(receipt, invoiceId)
|
||||
case .pending:
|
||||
alert = SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Purchase pending",
|
||||
message: "The purchase is awaiting approval. This build does not deliver purchases approved later."
|
||||
),
|
||||
id: "badgePurchasePending"
|
||||
)
|
||||
case .cancelled: break
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("BadgesPayView.purchase: \(String(describing: error))")
|
||||
await MainActor.run {
|
||||
purchasing = false
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Purchase error"),
|
||||
message: Text(verbatim: String(describing: error))
|
||||
),
|
||||
id: "badgePurchaseError"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [badges] store integration diagnostics - replaced by the issued badge once the service lands.
|
||||
private func showPurchasedAlert(_ receipt: BadgeStoreReceipt, _ invoiceId: UUID) {
|
||||
let returnedInvoice: String
|
||||
if let returned = receipt.invoiceId {
|
||||
returnedInvoice = returned == invoiceId ? "yes" : "mismatch: \(returned.uuidString)"
|
||||
} else {
|
||||
returnedInvoice = "none"
|
||||
}
|
||||
var lines = [
|
||||
"Product: \(receipt.productId)",
|
||||
"Invoice: \(invoiceId.uuidString)",
|
||||
"Invoice returned by Apple: \(returnedInvoice)",
|
||||
"Transaction: \(receipt.transactionId)"
|
||||
]
|
||||
if let environment = receipt.environment { lines.append("Environment: \(environment)") }
|
||||
lines.append("Signature: \(receipt.signatureVerified ? "verified" : "unverified")")
|
||||
lines.append("Token: \(receipt.jws.count) bytes")
|
||||
let summary = lines.joined(separator: "\n")
|
||||
// logged as well as shown: the alert races StoreKit's own sheets, the log always lands
|
||||
logger.debug("badge purchase succeeded\n\(summary)")
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Purchase successful"),
|
||||
message: Text(verbatim: summary),
|
||||
primaryButton: .default(Text(verbatim: "Copy token")) { UIPasteboard.general.string = receipt.jws },
|
||||
secondaryButton: .cancel(Text("Ok"))
|
||||
),
|
||||
id: "badgePurchased"
|
||||
)
|
||||
}
|
||||
|
||||
private var billingFooter: LocalizedStringKey {
|
||||
// TODO [badges] source the actual date from the purchase state machine when wired.
|
||||
var comps = DateComponents(); comps.year = 2026; comps.month = 7; comps.day = 22
|
||||
let stubDate = Calendar.current.date(from: comps) ?? Date()
|
||||
let date = DateFormatter.localizedString(from: stubDate, dateStyle: .long, timeStyle: .none)
|
||||
switch selectedPeriod {
|
||||
case .monthly, .annual: return "Renews on \(date). Cancel anytime."
|
||||
case .oneMonth: return "Ends on \(date)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesPayView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesPayView(level: .supporter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// BadgesRedeemCodeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import CodeScanner
|
||||
|
||||
private let badgeCodePrefix = "SB"
|
||||
private let badgeCodeBodyLength = 20
|
||||
private let badgeCodeGroupLength = 5
|
||||
|
||||
// Regroups what was typed; validity and the folding of ambiguous characters are core's alone.
|
||||
private func formatBadgeCodeInput(_ s: String) -> String {
|
||||
var normalized = ""
|
||||
for c in s.prefix(256).uppercased() {
|
||||
guard c.isLetter || c.isNumber else { continue }
|
||||
normalized.append(c)
|
||||
if normalized.count == badgeCodePrefix.count + badgeCodeBodyLength { break }
|
||||
}
|
||||
guard normalized.hasPrefix(badgeCodePrefix) else { return normalized }
|
||||
var groups = [badgeCodePrefix]
|
||||
var i = normalized.index(normalized.startIndex, offsetBy: badgeCodePrefix.count)
|
||||
while i < normalized.endIndex {
|
||||
let j = normalized.index(i, offsetBy: badgeCodeGroupLength, limitedBy: normalized.endIndex) ?? normalized.endIndex
|
||||
groups.append(String(normalized[i..<j]))
|
||||
i = j
|
||||
}
|
||||
return groups.joined(separator: "-")
|
||||
}
|
||||
|
||||
struct BadgesRedeemCodeView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State private var code = ""
|
||||
@State private var canonicalCode: String? = nil
|
||||
@State private var submitting = false
|
||||
@State private var showQRCodeScanner = true
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
entryContent(g)
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 20)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
}
|
||||
|
||||
if submitting {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.white)
|
||||
.opacity(0.7)
|
||||
.frame(width: 56, height: 56)
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func entryContent(_ g: GeometryProxy) -> some View {
|
||||
Text("Redeem code")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Paste the code you received.")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
codeField()
|
||||
|
||||
pasteButton()
|
||||
|
||||
ScannerInView(
|
||||
showQRCodeScanner: $showQRCodeScanner,
|
||||
scannerPaused: $submitting,
|
||||
processQRCode: processQRCode,
|
||||
scanMode: .oncePerCode,
|
||||
placeholderBackground: Color(.tertiarySystemFill)
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 12)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
submitButton()
|
||||
.padding(.vertical, 10)
|
||||
Color.clear
|
||||
.frame(height: 22)
|
||||
}
|
||||
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
|
||||
}
|
||||
|
||||
private func codeField() -> some View {
|
||||
TextField(text: $code) { Text(verbatim: "SB-XXXXX-XXXXX-XXXXX-XXXXX") }
|
||||
.font(.body.monospaced())
|
||||
.multilineTextAlignment(.center)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.characters)
|
||||
.disabled(submitting)
|
||||
.padding(EdgeInsets(top: 14, leading: 12, bottom: 14, trailing: 12))
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10.0)
|
||||
.onChange(of: code) { applyCodeInput($0) }
|
||||
}
|
||||
|
||||
private func pasteButton() -> some View {
|
||||
Button {
|
||||
if let pasted = UIPasteboard.general.string { applyCodeInput(pasted) }
|
||||
} label: {
|
||||
Text("Paste")
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(submitting)
|
||||
}
|
||||
|
||||
// a changed field re-enters through onChange with the formatted text, which is when it is parsed
|
||||
private func applyCodeInput(_ s: String) {
|
||||
let formatted = formatBadgeCodeInput(s)
|
||||
if formatted != code {
|
||||
code = formatted
|
||||
} else {
|
||||
canonicalCode = parseBadgeCode(formatted)
|
||||
}
|
||||
}
|
||||
|
||||
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
let formatted = formatBadgeCodeInput(r.string)
|
||||
if let canonical = parseBadgeCode(formatted) {
|
||||
// set here rather than through onChange, which runs after redeem() reads it
|
||||
code = formatted
|
||||
canonicalCode = canonical
|
||||
redeem()
|
||||
} else {
|
||||
showAlert(
|
||||
NSLocalizedString("Invalid QR code", comment: "alert title"),
|
||||
message: NSLocalizedString("The code you scanned is not a badge code.", comment: "alert message")
|
||||
)
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||
showAlert(
|
||||
NSLocalizedString("Invalid QR code", comment: "alert title"),
|
||||
message: String.localizedStringWithFormat(NSLocalizedString("Error scanning code: %@", comment: "alert message"), e.localizedDescription)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func submitButton() -> some View {
|
||||
let disabled = canonicalCode == nil || submitting
|
||||
return Button {
|
||||
redeem()
|
||||
} label: {
|
||||
Text("Redeem")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: disabled))
|
||||
.disabled(disabled)
|
||||
}
|
||||
|
||||
private func redeem() {
|
||||
guard let sending = canonicalCode, let user = chatModel.currentUser else { return }
|
||||
submitting = true
|
||||
Task {
|
||||
do {
|
||||
guard let redeemed = try await apiRedeemBadgeCode(user.userId, sending) else {
|
||||
await MainActor.run { submitting = false }
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
// set before dismissing: BadgesView then switches Support to Your Badge while this screen
|
||||
// still covers it, so the pop lands on Your Badge instead of showing the switch
|
||||
BadgeModel.shared.set(userId: user.userId, badgeState: redeemed.badgeState)
|
||||
chatModel.updateUser(redeemed.user)
|
||||
if let badgeState = redeemed.badgeState, !badgeState.shown {
|
||||
// a replay adds no purchase; a fresh code's badge can be retired on arrival
|
||||
let message = redeemed.newBadge
|
||||
? NSLocalizedString("The code was accepted, but the badge it grants has already ended.", comment: "alert message")
|
||||
: NSLocalizedString("This code has already been used.", comment: "alert message")
|
||||
showAlert(NSLocalizedString("Cannot redeem code", comment: "alert title"), message: message)
|
||||
} else {
|
||||
supporterBannerShown = true
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiRedeemBadgeCode: \(responseError(error))")
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
showAlert(NSLocalizedString("Cannot redeem code", comment: "alert title"), message: redeemErrorText(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesRedeemCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesRedeemCodeView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// BadgesSupportSimplexView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgesSupportSimplexView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
// set true when presented as a sheet root (from the chat-list banner) — that path doesn't
|
||||
// reserve nav-bar space like a NavigationLink push does, so the title lands too close to the top
|
||||
var showsAsSheet: Bool = false
|
||||
@State private var whyBuiltActive = false
|
||||
@State private var howItWorksActive = false
|
||||
@State private var chooseLevelActive = false
|
||||
@State private var redeemCodeActive = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text("Support SimpleX")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Get a badge to send larger files (2-5GB) that stay available longer (7-21 days), and to show it on your profile.")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// TODO [badges] restore whyBuiltButton() when in-app purchase lands: the level screen
|
||||
// returns to the flow and howItWorksButton() moves there, leaving this one alone here.
|
||||
howItWorksButton()
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
PhoneSupporterHero()
|
||||
.frame(maxWidth: g.size.width * 0.55)
|
||||
.layoutPriority(-1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
redeemCodeButton()
|
||||
.padding(.vertical, 10)
|
||||
getCodeButton()
|
||||
.frame(height: 22)
|
||||
}
|
||||
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, showsAsSheet ? 48 : 0)
|
||||
.padding(.bottom, 20)
|
||||
// .frame(height:) not minHeight — inside the banner sheet's NavigationView minHeight
|
||||
// would let the VStack expand past the visible area and inflate the hero.
|
||||
.frame(height: g.size.height)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
// the in-app purchase path, kept compiling and uncalled until payments return after the MVP
|
||||
private func chooseLevelButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
chooseLevelActive = true
|
||||
} label: {
|
||||
Text("Choose your level")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
|
||||
NavigationLink(isActive: $chooseLevelActive) {
|
||||
BadgesYourLevelView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func whyBuiltButton() -> some View {
|
||||
ZStack {
|
||||
Button { whyBuiltActive = true } label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "info.circle")
|
||||
Text("Why SimpleX is built.").fontWeight(.medium)
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
NavigationLink(isActive: $whyBuiltActive) {
|
||||
WhySimpleX(onboarding: false, titleColor: theme.colors.primary, createProfileNavLinkActive: .constant(false))
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func howItWorksButton() -> some View {
|
||||
ZStack {
|
||||
Button { howItWorksActive = true } label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "info.circle")
|
||||
Text("How badges protect your privacy").fontWeight(.medium)
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
NavigationLink(isActive: $howItWorksActive) {
|
||||
BadgesHowItWorksView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func redeemCodeButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
redeemCodeActive = true
|
||||
} label: {
|
||||
Text("Redeem badge code")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
|
||||
NavigationLink(isActive: $redeemCodeActive) {
|
||||
BadgesRedeemCodeView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func getCodeButton() -> some View {
|
||||
Button {
|
||||
openExternalLink(URL(string: "https://simplex.chat/badges/")!)
|
||||
} label: {
|
||||
Text("Get your code")
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PhoneSupporterHero: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
var body: some View {
|
||||
#if SIMPLEX_ASSETS
|
||||
Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity)
|
||||
#else
|
||||
ZStack {
|
||||
let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5)
|
||||
LinearGradient(
|
||||
stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
|
||||
startPoint: gp.start,
|
||||
endPoint: gp.end
|
||||
)
|
||||
Image("badge-supporter")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 96)
|
||||
}
|
||||
.aspectRatio(1.0, contentMode: .fit)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24))
|
||||
.frame(maxWidth: .infinity)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesSupportSimplexView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesSupportSimplexView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// BadgesView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 11.09.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgesView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject private var badgeModel = BadgeModel.shared
|
||||
var showsAsSheet: Bool = false
|
||||
|
||||
private var shownBadge: BadgeState? {
|
||||
guard badgeModel.userId == chatModel.currentUser?.userId else { return nil }
|
||||
if let badgeState = badgeModel.badgeState, badgeState.shown { return badgeState }
|
||||
return nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let badgeState = shownBadge {
|
||||
BadgesYourBadgeView(badgeState: badgeState, showsAsSheet: showsAsSheet)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
BadgesSupportSimplexView(showsAsSheet: showsAsSheet)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: shownBadge != nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// BadgesYourBadgeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 11.09.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgesYourBadgeView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
let badgeState: BadgeState
|
||||
var showsAsSheet: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if showsAsSheet {
|
||||
Text("Your badge")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 48)
|
||||
}
|
||||
|
||||
List {
|
||||
Section {
|
||||
BadgeSummary(badgeState: badgeState)
|
||||
}
|
||||
Section {
|
||||
Text(badgeState.paidThroughText)
|
||||
} header: {
|
||||
Text("Ends")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Prepaid months have no billing date. The badge is reissued each month from the balance you already paid for, and ends when it runs out.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
Section {
|
||||
NavigationLink {
|
||||
BadgesHowItWorksView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("info.circle", color: theme.colors.secondary) {
|
||||
Text("How badges protect your privacy")
|
||||
}
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
Section(header: Text("Credential").foregroundColor(theme.colors.secondary)) {
|
||||
if let badge = chatModel.currentUser?.profile.localBadge {
|
||||
infoRow("Status", badge.status.rawValue)
|
||||
infoRow("Expires", DateFormatter.localizedString(from: badge.badge.badgeExpiry, dateStyle: .medium, timeStyle: .short))
|
||||
}
|
||||
infoRow("Months left", "\(badgeState.monthsLeft)")
|
||||
infoRow("Purchase ID", "\(badgeState.badgePurchaseId)")
|
||||
Button("Copy purchase key") {
|
||||
UIPasteboard.general.string = badgeState.purchaseKey
|
||||
}
|
||||
NavigationLink {
|
||||
BadgesLedgerView(badgeState: badgeState)
|
||||
} label: {
|
||||
Text("Badge ledger")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(showsAsSheet ? "" : "Your badge")
|
||||
.navigationBarTitleDisplayMode(showsAsSheet ? .inline : .large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgeSummary: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let badgeState: BadgeState
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Image(badgeImageName(badgeState.badgeType))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 68, height: 68)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(badgeTypeName(badgeState.badgeType))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("shown on your profile")
|
||||
.font(.footnote)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// BadgesYourLevelView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum BadgeLevel: String, CaseIterable, Identifiable {
|
||||
case supporter
|
||||
case legend
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .supporter: "Supporter"
|
||||
case .legend: "Legend"
|
||||
}
|
||||
}
|
||||
|
||||
var filesDescription: LocalizedStringKey {
|
||||
switch self {
|
||||
case .supporter: "Send 2GB files"
|
||||
case .legend: "Send 5GB files"
|
||||
}
|
||||
}
|
||||
|
||||
var tagline: LocalizedStringKey {
|
||||
switch self {
|
||||
case .supporter: "Optional profile badge\nand 2GB files"
|
||||
case .legend: "Optional profile badge\nand 5GB files"
|
||||
}
|
||||
}
|
||||
|
||||
var badgeType: BadgeType {
|
||||
switch self {
|
||||
case .supporter: .supporter
|
||||
case .legend: .legend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesYourLevelView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject private var store = BadgeStore.shared
|
||||
@State private var selectedLevel: BadgeLevel = .supporter
|
||||
@State private var continueActive = false
|
||||
@State private var howItWorksActive = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text("Your level")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
BadgeUserPreview(level: selectedLevel) {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.body)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
// fixedSize + maxHeight on the cards so both match the taller one when a
|
||||
// store price wraps in one of them
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
levelCard(.supporter)
|
||||
levelCard(.legend)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
continueButton()
|
||||
.padding(.vertical, 10)
|
||||
howItWorksButton()
|
||||
.frame(height: 22)
|
||||
}
|
||||
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 0)
|
||||
.padding(.bottom, 20)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task { await store.load() }
|
||||
}
|
||||
|
||||
private func levelCard(_ level: BadgeLevel) -> some View {
|
||||
let isSelected = level == selectedLevel
|
||||
return Button {
|
||||
selectedLevel = level
|
||||
} label: {
|
||||
VStack(spacing: 10) {
|
||||
Image(badgeImageName(level.badgeType))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 60, height: 60)
|
||||
.padding(.top, 20)
|
||||
Text(level.title)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
Text(level.filesDescription)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
BadgePeriod.monthly.priceText(store.price(level, .monthly))
|
||||
.font(.body)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(isSelected ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func continueButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
continueActive = true
|
||||
} label: {
|
||||
Text("Continue")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
|
||||
NavigationLink(isActive: $continueActive) {
|
||||
BadgesPayView(level: selectedLevel)
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func howItWorksButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
howItWorksActive = true
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "info.circle")
|
||||
Text("How badges protect your privacy").fontWeight(.medium)
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $howItWorksActive) {
|
||||
BadgesHowItWorksView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesYourLevelView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesYourLevelView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// SupportSimpleXBanner.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.07.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SupportSimpleXBanner: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
var title: LocalizedStringKey = "Support SimpleX"
|
||||
var subtitle: LocalizedStringKey = "Get badge + better files"
|
||||
let onTap: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
|
||||
private let cardCornerRadius: CGFloat = 16
|
||||
// grows with Dynamic Type but never shrinks below the default so small-font users see the same
|
||||
// banner as today; hero stays fixed so its above-card overhang shrinks at very large fonts
|
||||
@ScaledMetric(relativeTo: .body) private var scaledCardHeight: CGFloat = 72
|
||||
private var cardHeight: CGFloat { max(72, scaledCardHeight) }
|
||||
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
|
||||
private let cardLeadingPadding: CGFloat = 16
|
||||
private let cardTrailingPadding: CGFloat = 8
|
||||
private let heroWidth: CGFloat = 110
|
||||
// shorter than the natural drawn height so .clipped() slices the phone body at card bottom
|
||||
private let heroVisibleHeight: CGFloat = 108
|
||||
// hero right edge sits exactly at the dismiss X's left edge (X trailing 16 + width 12)
|
||||
private let heroTrailingPadding: CGFloat = 28
|
||||
private let textToHeroGap: CGFloat = 6
|
||||
|
||||
var body: some View {
|
||||
// Card is the Button; hero is an overlay so it can extend above the card top without
|
||||
// affecting layout size. Dismiss X is a ZStack sibling anchored to the card's top-right.
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.lineLimit(2)
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer(minLength: heroWidth + heroTrailingPadding + textToHeroGap)
|
||||
}
|
||||
.padding(.leading, cardLeadingPadding)
|
||||
.padding(.trailing, cardTrailingPadding)
|
||||
.padding(.vertical, 12)
|
||||
.frame(minHeight: cardHeight)
|
||||
.background(gradientBackground())
|
||||
.clipShape(RoundedRectangle(cornerRadius: cardCornerRadius))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
heroThumbnail()
|
||||
.padding(.trailing, heroTrailingPadding)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
Image(systemName: "multiply")
|
||||
.foregroundColor(colorScheme == .dark ? theme.colors.onBackground : theme.colors.secondary)
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 4)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func heroThumbnail() -> some View {
|
||||
#if SIMPLEX_ASSETS
|
||||
// draws at natural aspect, top-aligned in a shorter slot; .clipped() cuts the overflow at card bottom
|
||||
Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: heroWidth, height: heroVisibleHeight, alignment: .top)
|
||||
.clipped()
|
||||
#else
|
||||
Image("badge-supporter")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 48, height: 48)
|
||||
.padding(.vertical, (cardHeight - 48) / 2)
|
||||
.padding(.trailing, 12)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func gradientBackground() -> some View {
|
||||
// Asymmetric scale: start (dark end) pushed further below the card than the end (warm) is
|
||||
// above, so the card's middle lands at the bright/mid-transition stop instead of the dark
|
||||
// navy region. Keeps the small warm accent at top-right.
|
||||
GeometryReader { geo in
|
||||
let aspect = max(geo.size.height, 1) / max(geo.size.width, 1)
|
||||
let startScale: CGFloat = colorScheme == .light ? 2.5 : 3.0
|
||||
let endScale: CGFloat = colorScheme == .light ? 1.7 : 2.1
|
||||
let gp = OnboardingCardView.gradientPoints(aspectRatio: aspect, scale: 1.0)
|
||||
let start = UnitPoint(x: 0.5 + (gp.start.x - 0.5) * startScale, y: 0.5 + (gp.start.y - 0.5) * startScale)
|
||||
let end = UnitPoint(x: 0.5 + (gp.end.x - 0.5) * endScale, y: 0.5 + (gp.end.y - 0.5) * endScale)
|
||||
return LinearGradient(
|
||||
stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
|
||||
startPoint: start,
|
||||
endPoint: end
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportSimpleXBanner_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SupportSimpleXBanner(onTap: {}, onDismiss: {})
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ struct CIFileView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
let file: CIFile?
|
||||
let meta: CIMeta
|
||||
let senderProfile: LocalProfile?
|
||||
var smallViewSize: CGFloat?
|
||||
|
||||
var body: some View {
|
||||
@@ -91,19 +90,15 @@ struct CIFileView: View {
|
||||
if let file = file {
|
||||
switch (file.fileStatus) {
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
if fileSizeValid(file, senderProfile) {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: getMaxFileSize(file.fileProtocol, senderProfile), countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Your contact sent a file that is larger than currently supported maximum size (\(prettyMaxFileSize))."
|
||||
)
|
||||
}
|
||||
case .rcvAccepted:
|
||||
switch file.fileProtocol {
|
||||
@@ -171,7 +166,7 @@ struct CIFileView: View {
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if !fileSizeValid(file, senderProfile) {
|
||||
if !fileSizeValid(file) {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
} else if file.expired {
|
||||
fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
@@ -235,13 +230,27 @@ struct CIFileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func fileSizeValid(_ file: CIFile?, _ senderProfile: LocalProfile?) -> Bool {
|
||||
// the core decides whether a received file is above the size the sender's badge allows
|
||||
func fileSizeValid(_ file: CIFile?) -> Bool {
|
||||
if let file = file {
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol, senderProfile)
|
||||
return file.fileProhibited == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func showProhibitedFileAlert(_ file: CIFile, _ prohibited: FileProhibited) {
|
||||
let badgeIssue = switch prohibited.badgeStatus {
|
||||
case .none, .some(.active): ""
|
||||
case .some(.expired), .some(.expiredOld): NSLocalizedString("Contact's badge expired.", comment: "file alert")
|
||||
case .some(.failed): NSLocalizedString("Contact's badge verification failed.", comment: "file alert")
|
||||
case .some(.unknownKey): NSLocalizedString("No key to verify contact's badge.", comment: "file alert")
|
||||
}
|
||||
showAlert(
|
||||
NSLocalizedString("Large file!", comment: "file alert title"),
|
||||
message: largeFileMessage(file.fileSize, badgeIssue: badgeIssue)
|
||||
)
|
||||
}
|
||||
|
||||
func saveCryptoFile(_ fileSource: CryptoFile) {
|
||||
if let cfArgs = fileSource.cryptoArgs {
|
||||
let url = getAppFilePath(fileSource.filePath)
|
||||
|
||||
@@ -14,7 +14,6 @@ import SimpleXChat
|
||||
struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
let chatItem: ChatItem
|
||||
let senderProfile: LocalProfile?
|
||||
var scrollToItem: ((ChatItem.ID) -> Void)? = nil
|
||||
var preview: UIImage?
|
||||
let maxWidth: CGFloat
|
||||
@@ -52,18 +51,14 @@ struct CIImageView: View {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
if fileSizeValid(file, senderProfile) {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: getMaxFileSize(file.fileProtocol, senderProfile), countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Your contact sent a file that is larger than currently supported maximum size (\(prettyMaxFileSize))."
|
||||
)
|
||||
}
|
||||
case .rcvAccepted:
|
||||
switch file.fileProtocol {
|
||||
@@ -152,7 +147,7 @@ struct CIImageView: View {
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file, senderProfile) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case .rcvTransfer: progressView()
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
|
||||
@@ -16,7 +16,6 @@ import Combine
|
||||
struct CIVideoView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
private let chatItem: ChatItem
|
||||
private let senderProfile: LocalProfile?
|
||||
private let preview: UIImage?
|
||||
@State private var duration: Int
|
||||
@State private var progress: Int = 0
|
||||
@@ -36,9 +35,8 @@ struct CIVideoView: View {
|
||||
private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 }
|
||||
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
|
||||
|
||||
init(chatItem: ChatItem, senderProfile: LocalProfile?, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding<Bool>) {
|
||||
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding<Bool>) {
|
||||
self.chatItem = chatItem
|
||||
self.senderProfile = senderProfile
|
||||
self.preview = preview
|
||||
self._duration = State(initialValue: duration)
|
||||
self.maxWidth = maxWidth
|
||||
@@ -368,7 +366,7 @@ struct CIVideoView: View {
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showFileErrorAlert(sndFileError, temporary: true)
|
||||
})
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file, senderProfile) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvInvitation: fileIcon(file.expired && fileSizeValid(file) ? "xmark" : "arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
|
||||
@@ -423,18 +421,14 @@ struct CIVideoView: View {
|
||||
|
||||
// TODO encrypt: where file size is checked?
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
||||
if fileSizeValid(file, senderProfile) {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user, file.fileId, false, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: getMaxFileSize(file.fileProtocol, senderProfile), countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Your contact sent a file that is larger than currently supported maximum size (\(prettyMaxFileSize))."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
switch (chatItem.content.msgContent) {
|
||||
case let .image(text, _):
|
||||
CIImageView(chatItem: chatItem, senderProfile: ciSenderProfile(chatItem, chat.chatInfo), scrollToItem: scrollToItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery)
|
||||
CIImageView(chatItem: chatItem, scrollToItem: scrollToItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
@@ -170,7 +170,7 @@ struct FramedItemView: View {
|
||||
ciMsgContentView(chatItem)
|
||||
}
|
||||
case let .video(text, _, duration):
|
||||
CIVideoView(chatItem: chatItem, senderProfile: ciSenderProfile(chatItem, chat.chatInfo), preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery)
|
||||
CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
@@ -387,7 +387,7 @@ struct FramedItemView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View {
|
||||
CIFileView(chat: chat, file: chatItem.file, meta: chatItem.meta, senderProfile: ciSenderProfile(chatItem, chat.chatInfo))
|
||||
CIFileView(chat: chat, file: chatItem.file, meta: chatItem.meta)
|
||||
.overlay(DetermineWidth())
|
||||
if text != "" || ci.meta.isLive {
|
||||
ciMsgContentView (chatItem)
|
||||
|
||||
@@ -2322,7 +2322,7 @@ struct ChatView: View {
|
||||
} else {
|
||||
saveButton(file: fileSource)
|
||||
}
|
||||
} else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file, ciSenderProfile(ci, chat.chatInfo)) {
|
||||
} else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file) {
|
||||
downloadButton(file: file)
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice && !live {
|
||||
|
||||
@@ -668,10 +668,9 @@ struct ComposeView: View {
|
||||
fileSize <= maxFileSize {
|
||||
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: maxFileSize, countStyle: .binary)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
||||
showAlert(
|
||||
NSLocalizedString("Large file!", comment: "file alert title"),
|
||||
message: largeFileMessage(Int64(fileSize ?? 0), incognito: sendIncognito, badgeIssue: expiredBadgeReason(Int64(fileSize ?? 0), sendProfile))
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
@@ -1266,12 +1265,15 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var maxFileSize: Int64 {
|
||||
// the user's active badge raises the limit, but not in incognito chats where no badge is presented
|
||||
let incognito = chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault
|
||||
return getMaxFileSize(.xftp, incognito ? nil : chatModel.currentUser?.profile)
|
||||
// no badge is presented in incognito chats, so it does not raise the limit there
|
||||
private var sendIncognito: Bool {
|
||||
chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault
|
||||
}
|
||||
|
||||
private var sendProfile: LocalProfile? { sendIncognito ? nil : chatModel.currentUser?.profile }
|
||||
|
||||
private var maxFileSize: Int64 { getMaxFileSize(.xftp, sendProfile) }
|
||||
|
||||
// Spec: spec/client/compose.md#sendLiveMessage
|
||||
private func sendLiveMessage() async {
|
||||
let typedMsg = composeState.message
|
||||
|
||||
@@ -154,9 +154,11 @@ struct UserPickerSheetView: View {
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@ObservedObject private var badgeModel = BadgeModel.shared
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var activeUserPickerSheet: UserPickerSheet?
|
||||
@State private var showNewChatSheet = false
|
||||
@State private var showGetStakeSheet = false
|
||||
@State private var searchMode = false
|
||||
@FocusState private var searchFocussed
|
||||
@State private var searchText = ""
|
||||
@@ -174,7 +176,11 @@ struct ChatListView: View {
|
||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
||||
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
||||
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
|
||||
@AppStorage(DEFAULT_SUPPORTER_BANNER_SHOWN) private var supporterBannerShown = false
|
||||
@AppStorage(DEFAULT_GET_STAKE_BANNER_TAPPED) private var getStakeBannerTapped = false
|
||||
@AppStorage(DEFAULT_GET_STAKE_BANNER_DISMISSED) private var getStakeBannerDismissed = false
|
||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
@State private var showBadgesSheet = false
|
||||
|
||||
// Spec: spec/client/chat-list.md#body
|
||||
var body: some View {
|
||||
@@ -211,6 +217,15 @@ struct ChatListView: View {
|
||||
NewChatSheet()
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
.appSheet(isPresented: $showBadgesSheet) {
|
||||
NavigationView {
|
||||
BadgesView(showsAsSheet: true)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.appSheet(isPresented: $showGetStakeSheet) {
|
||||
GetStakeView(fromSettings: false, showFirstImage: true)
|
||||
}
|
||||
.onChange(of: activeUserPickerSheet) {
|
||||
if $0 != nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
@@ -363,8 +378,42 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// the onboarding cards replace the whole chat list, and the support-ended banner lives in the
|
||||
// list - a lapsed supporter is not a newcomer, and must be told even with no conversations yet
|
||||
private var shouldShowOnboarding: Bool {
|
||||
!addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations
|
||||
!addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations && !supportEnded
|
||||
}
|
||||
|
||||
private var supportEnded: Bool {
|
||||
badgeModel.alert?.kind == .supportEnded && badgeModel.userId == chatModel.currentUser?.userId
|
||||
}
|
||||
|
||||
private var hasShownBadge: Bool {
|
||||
badgeModel.badgeState?.shown == true && badgeModel.userId == chatModel.currentUser?.userId
|
||||
}
|
||||
|
||||
private func showSupportEndedDismissAlert() {
|
||||
showAlert(NSLocalizedString("Your badge expired", comment: "alert title")) {
|
||||
[
|
||||
UIAlertAction(title: NSLocalizedString("Remind me later", comment: "alert button"), style: .default) { _ in
|
||||
Task { await ackBadgeAlert(snooze: true) }
|
||||
},
|
||||
UIAlertAction(title: NSLocalizedString("Dismiss", comment: "alert button"), style: .default) { _ in
|
||||
Task { await ackBadgeAlert(snooze: false) }
|
||||
},
|
||||
cancelAlertAction
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private func showSupportSimpleXDismissAlert() {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Support SimpleX", comment: "alert title"),
|
||||
message: NSLocalizedString("You can support SimpleX later in Settings.", comment: "alert message"),
|
||||
buttonTitle: NSLocalizedString("Ok", comment: "alert button"),
|
||||
buttonAction: { withAnimation { supporterBannerShown = true } },
|
||||
cancelButton: false
|
||||
)
|
||||
}
|
||||
|
||||
private var hasConversations: Bool {
|
||||
@@ -382,14 +431,26 @@ struct ChatListView: View {
|
||||
|
||||
@ViewBuilder private var chatList: some View {
|
||||
if shouldShowOnboarding {
|
||||
ConnectOnboardingView()
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.modifier(ThemedBackground())
|
||||
VStack(spacing: 0) {
|
||||
ConnectOnboardingView()
|
||||
if isInUS && !getStakeBannerDismissed {
|
||||
GetStakeBanner(showDismiss: false, onTap: openGetStake, onDismiss: {})
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.modifier(ThemedBackground())
|
||||
} else {
|
||||
chatListContent
|
||||
}
|
||||
}
|
||||
|
||||
private func openGetStake() {
|
||||
getStakeBannerTapped = true
|
||||
showGetStakeSheet = true
|
||||
}
|
||||
|
||||
private var chatListContent: some View {
|
||||
let cs = filteredChats()
|
||||
return ZStack {
|
||||
@@ -418,6 +479,41 @@ struct ChatListView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
// one slot: a badge the user paid for ending outranks the pitch to get one
|
||||
if supportEnded, let alert = badgeModel.alert {
|
||||
SupportSimpleXBanner(
|
||||
title: "Your badge expired",
|
||||
subtitle: "Your badge expired on \(alert.dateText).",
|
||||
onTap: { showBadgesSheet = true },
|
||||
onDismiss: showSupportEndedDismissAlert
|
||||
)
|
||||
.padding(.vertical, 3)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
} else if !supporterBannerShown && !hasShownBadge && chatModel.chats.count > 3 {
|
||||
SupportSimpleXBanner(
|
||||
onTap: { showBadgesSheet = true },
|
||||
onDismiss: showSupportSimpleXDismissAlert
|
||||
)
|
||||
.padding(.vertical, 3)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
} else if isInUS && !getStakeBannerDismissed {
|
||||
GetStakeBanner(
|
||||
showDismiss: getStakeBannerTapped && !chatModel.chats.isEmpty,
|
||||
onTap: openGetStake,
|
||||
onDismiss: { withAnimation { getStakeBannerDismissed = true } }
|
||||
)
|
||||
.padding(.vertical, 3)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
@@ -518,7 +614,7 @@ struct ChatListView: View {
|
||||
VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() }
|
||||
VoiceItemState.smallView = [:]
|
||||
}
|
||||
|
||||
|
||||
// Spec: spec/client/chat-list.md#filteredChats
|
||||
private func filteredChats() -> [Chat] {
|
||||
if !searchChatFilteredBySimplexLink.isEmpty {
|
||||
|
||||
@@ -426,11 +426,11 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case let .image(_, image):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIImageView(chatItem: ci, senderProfile: ciSenderProfile(ci, chat.chatInfo), preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
}
|
||||
case let .video(_,image, duration):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIVideoView(chatItem: ci, senderProfile: ciSenderProfile(ci, chat.chatInfo), preview: imageFromBase64(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)
|
||||
}
|
||||
case let .voice(_, duration):
|
||||
smallContentPreviewVoice(size: dynamicMediaSize) {
|
||||
@@ -438,7 +438,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case .file:
|
||||
smallContentPreviewFile(size: dynamicMediaSize) {
|
||||
CIFileView(chat: chat, file: ci.file, meta: ci.meta, senderProfile: ciSenderProfile(ci, chat.chatInfo), smallViewSize: dynamicMediaSize)
|
||||
CIFileView(chat: chat, file: ci.file, meta: ci.meta, smallViewSize: dynamicMediaSize)
|
||||
}
|
||||
case let .chat(_, chatLink, ownerSig):
|
||||
smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// GetStakeBanner.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 21.09.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
// Spec: spec/client/chat-list.md#GetStakeBanner
|
||||
struct GetStakeBanner: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var showDismiss: Bool
|
||||
let onTap: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Get a stake in SimpleX Chat!")
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.lineLimit(2)
|
||||
Text("Invest on Wefunder from $100")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer(minLength: 6)
|
||||
Image("wefunder_logo")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 48, height: 48)
|
||||
// roughly where the phone of the badge banner's hero is: 40pt from the card edge, less its 8pt inset
|
||||
.padding(.trailing, 32)
|
||||
}
|
||||
.modifier(BannerCard())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if showDismiss {
|
||||
BannerDismissButton(onDismiss: onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BannerCard: ViewModifier {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
// grows with Dynamic Type but never shrinks below the default, so small-font users see the same card
|
||||
@ScaledMetric(relativeTo: .body) private var scaledCardHeight: CGFloat = 72
|
||||
private var cardHeight: CGFloat { max(72, scaledCardHeight) }
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
// the leading padding matches OneHandUICard's segment icon, so the text aligns with it in the list
|
||||
.padding(.leading, 16)
|
||||
.padding(.trailing, 8)
|
||||
.padding(.vertical, 12)
|
||||
.frame(minHeight: cardHeight)
|
||||
.background(gradientBackground())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
|
||||
private func gradientBackground() -> some View {
|
||||
// Asymmetric scale: start (dark end) pushed further below the card than the end (warm) is
|
||||
// above, so the card's middle lands at the bright/mid-transition stop instead of the dark
|
||||
// navy region. Keeps the small warm accent at top-right.
|
||||
GeometryReader { geo in
|
||||
let aspect = max(geo.size.height, 1) / max(geo.size.width, 1)
|
||||
let startScale: CGFloat = colorScheme == .light ? 2.5 : 3.0
|
||||
let endScale: CGFloat = colorScheme == .light ? 1.7 : 2.1
|
||||
let gp = OnboardingCardView.gradientPoints(aspectRatio: aspect, scale: 1.0)
|
||||
let start = UnitPoint(x: 0.5 + (gp.start.x - 0.5) * startScale, y: 0.5 + (gp.start.y - 0.5) * startScale)
|
||||
let end = UnitPoint(x: 0.5 + (gp.end.x - 0.5) * endScale, y: 0.5 + (gp.end.y - 0.5) * endScale)
|
||||
return LinearGradient(
|
||||
stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
|
||||
startPoint: start,
|
||||
endPoint: end
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BannerDismissButton: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
let onDismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "multiply")
|
||||
.foregroundColor(colorScheme == .dark ? theme.colors.onBackground : theme.colors.secondary)
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 4)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GetStakeBanner(showDismiss: true, onTap: {}, onDismiss: {})
|
||||
.padding()
|
||||
}
|
||||
@@ -42,7 +42,8 @@ struct UserPicker: View {
|
||||
HStack {
|
||||
ProfileImage(imageStr: user.image, size: imageSize, color: Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.padding(.trailing, 6)
|
||||
profileName(user).lineLimit(1)
|
||||
NameWithBadge(profileName(user), user.profile.localBadge, .title2)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(rowPadding)
|
||||
.frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading)
|
||||
|
||||
@@ -95,7 +95,7 @@ struct NameBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func badgeImageName(_ t: BadgeType) -> String {
|
||||
func badgeImageName(_ t: BadgeType) -> String {
|
||||
switch t {
|
||||
case .legend: "badge-legend"
|
||||
case .investor: "badge-investor"
|
||||
@@ -147,8 +147,7 @@ func showBadgeInfoAlert(_ name: String, _ badge: LocalBadge) {
|
||||
)
|
||||
default:
|
||||
// a verified badge's type is signed and can't be faked, so the real (possibly unknown) type name is the title
|
||||
let t = badge.badge.badgeType.text
|
||||
let title = t.prefix(1).uppercased() + t.dropFirst()
|
||||
let title = badgeTypeName(badge.badge.badgeType)
|
||||
if case .investor = badge.badge.badgeType {
|
||||
let message = String.localizedStringWithFormat(NSLocalizedString("%@ invested in SimpleX Chat crowdfunding.", comment: "badge alert"), name)
|
||||
showAlert(title, message: message) {
|
||||
@@ -172,3 +171,13 @@ func showBadgeInfoAlert(_ name: String, _ badge: LocalBadge) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verbatim for an unknown type: it is the service's string, and must not be looked up as a localised key
|
||||
func badgeTypeName(_ t: BadgeType) -> String {
|
||||
switch t {
|
||||
case .supporter: NSLocalizedString("Supporter", comment: "badge type")
|
||||
case .legend: NSLocalizedString("Legend", comment: "badge type")
|
||||
case .investor: NSLocalizedString("Investor", comment: "badge type")
|
||||
case let .unknown(s): s
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,6 +739,7 @@ struct ScannerInView: View {
|
||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
var scanMode: ScanMode = .continuous
|
||||
var placeholderBackground = Color(uiColor: .secondarySystemGroupedBackground)
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -777,7 +778,7 @@ struct ScannerInView: View {
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.fill(placeholderBackground)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
@@ -65,6 +65,7 @@ struct WhySimpleX: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var onboarding: Bool
|
||||
var titleColor: Color? = nil
|
||||
@Binding var createProfileNavLinkActive: Bool
|
||||
|
||||
var body: some View {
|
||||
@@ -74,6 +75,7 @@ struct WhySimpleX: View {
|
||||
Text("You were born without an account")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.foregroundColor(titleColor)
|
||||
.padding(.top)
|
||||
Text("Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life.")
|
||||
Text("Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible.")
|
||||
|
||||
@@ -698,6 +698,20 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
))
|
||||
]
|
||||
),
|
||||
// TODO [badges] restore when badges ship, with final copy and the Read more link.
|
||||
/*
|
||||
VersionDescription(
|
||||
version: "v7.1",
|
||||
post: nil,
|
||||
features: [
|
||||
.view(FeatureView(
|
||||
icon: nil,
|
||||
title: "Supporter badge",
|
||||
view: { SupporterBadgeWhatsNew() }
|
||||
))
|
||||
]
|
||||
),
|
||||
*/
|
||||
]
|
||||
|
||||
private let lastVersion = versionDescriptions.last!.version
|
||||
@@ -712,6 +726,42 @@ func shouldShowWhatsNew() -> Bool {
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
fileprivate struct SupporterBadgeWhatsNew: View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Supporter badge ❤️")
|
||||
.font(.title3)
|
||||
.bold()
|
||||
Text("Help keep the network running — send files up to 2 GB.")
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(10)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
heroThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func heroThumbnail() -> some View {
|
||||
#if SIMPLEX_ASSETS
|
||||
Image(colorScheme == .light ? "phone-supporter" : "phone-supporter-light")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 110, height: 110)
|
||||
#else
|
||||
Image("badge-supporter")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 72, height: 72)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct NewOperatorsView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
@@ -795,7 +845,7 @@ fileprivate struct InvestInSimpleXChat: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.sheet(isPresented: $showGetStakeSheet) {
|
||||
GetStakeView(fromSettings: false)
|
||||
GetStakeView(fromSettings: false, showFirstImage: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -835,6 +885,7 @@ struct GetStakeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var fromSettings: Bool
|
||||
var showFirstImage: Bool
|
||||
|
||||
var body: some View {
|
||||
ZoomablePageView {
|
||||
@@ -844,7 +895,7 @@ struct GetStakeView: View {
|
||||
.bold()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.if(!fromSettings) { $0.padding(.top) }
|
||||
if fromSettings {
|
||||
if showFirstImage {
|
||||
slideImage(getStakeSlides[0])
|
||||
}
|
||||
(Text(verbatim: getStakeSlides[0].text) + Text(verbatim: " Learn more and invest on Wefunder.").bold().foregroundColor(.accentColor))
|
||||
|
||||
@@ -56,6 +56,9 @@ let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
|
||||
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
|
||||
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
|
||||
let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown"
|
||||
let DEFAULT_SUPPORTER_BANNER_SHOWN = "supporterBannerShown"
|
||||
let DEFAULT_GET_STAKE_BANNER_TAPPED = "getStakeBannerTapped"
|
||||
let DEFAULT_GET_STAKE_BANNER_DISMISSED = "getStakeBannerDismissed"
|
||||
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
|
||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||
@@ -117,6 +120,9 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CHAT_ITEM_TAIL: true,
|
||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
|
||||
DEFAULT_ADDRESS_CREATION_CARD_SHOWN: false,
|
||||
DEFAULT_SUPPORTER_BANNER_SHOWN: false,
|
||||
DEFAULT_GET_STAKE_BANNER_TAPPED: false,
|
||||
DEFAULT_GET_STAKE_BANNER_DISMISSED: false,
|
||||
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
@@ -148,6 +154,9 @@ let hintDefaults = [
|
||||
DEFAULT_LA_NOTICE_SHOWN,
|
||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN,
|
||||
DEFAULT_ADDRESS_CREATION_CARD_SHOWN,
|
||||
DEFAULT_SUPPORTER_BANNER_SHOWN,
|
||||
DEFAULT_GET_STAKE_BANNER_TAPPED,
|
||||
DEFAULT_GET_STAKE_BANNER_DISMISSED,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
|
||||
DEFAULT_SIGN_MESSAGE_ALERT_SHOWN,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
|
||||
@@ -341,6 +350,22 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink {
|
||||
BadgesView()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
let badgeType = chatModel.currentUser?.profile.localBadge?.badge.badgeType ?? .supporter
|
||||
ZStack(alignment: .leading) {
|
||||
Image(badgeImageName(badgeType))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
Text("Supporter perks").padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Advanced settings").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
NetworkAndServers()
|
||||
@@ -375,7 +400,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text(verbatim: "v\(appVersion ?? "?")")
|
||||
}
|
||||
@@ -384,7 +409,7 @@ struct SettingsView: View {
|
||||
if isInUS {
|
||||
Section(header: Text("You can now invest in SimpleX Chat").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
GetStakeView(fromSettings: true)
|
||||
GetStakeView(fromSettings: true, showFirstImage: true)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
} label: {
|
||||
settingsRow("dollarsign.circle", color: theme.colors.secondary) { Text("Crowdfunding on Wefunder") }
|
||||
|
||||
@@ -346,7 +346,7 @@ struct UserProfilesView: View {
|
||||
HStack {
|
||||
ProfileImage(imageStr: user.image, size: 38)
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
NameWithBadge(Text(user.chatViewName), user.profile.localBadge)
|
||||
Spacer()
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground)
|
||||
|
||||
@@ -97,37 +97,42 @@ class ShareModel: ObservableObject {
|
||||
if let e = initChat(with: dbKey) {
|
||||
await MainActor.run { errorAlert = e }
|
||||
} else {
|
||||
// Load Chats
|
||||
Task {
|
||||
switch fetchChats() {
|
||||
case let .success(chats):
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = imageFromBase64(profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
switch activeUser() {
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
case let .success(user):
|
||||
// Load Chats
|
||||
Task {
|
||||
switch fetchChats(user) {
|
||||
case let .success(chats):
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = imageFromBase64(profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
self.chats = chats
|
||||
self.profileImages = profileImages
|
||||
withAnimation { isLoaded = true }
|
||||
}
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
}
|
||||
await MainActor.run {
|
||||
self.chats = chats
|
||||
self.profileImages = profileImages
|
||||
withAnimation { isLoaded = true }
|
||||
}
|
||||
case let .failure(error):
|
||||
await MainActor.run { errorAlert = error }
|
||||
}
|
||||
}
|
||||
// Process Attachment
|
||||
Task {
|
||||
switch await getSharedContent(self.itemProvider!) {
|
||||
case let .success(chatItemContent):
|
||||
await MainActor.run {
|
||||
self.sharedContent = chatItemContent
|
||||
self.bottomBar = .sendButton
|
||||
if case let .text(string) = chatItemContent { comment = string }
|
||||
// Process Attachment
|
||||
Task {
|
||||
switch await getSharedContent(self.itemProvider!, user.profile) {
|
||||
case let .success(chatItemContent):
|
||||
await MainActor.run {
|
||||
self.sharedContent = chatItemContent
|
||||
self.bottomBar = .sendButton
|
||||
if case let .text(string) = chatItemContent { comment = string }
|
||||
}
|
||||
case let .failure(errorAlert):
|
||||
await MainActor.run { self.errorAlert = errorAlert }
|
||||
}
|
||||
case let .failure(errorAlert):
|
||||
await MainActor.run { self.errorAlert = errorAlert }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +258,7 @@ class ShareModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchChats() -> Result<Array<SEChatData>, ErrorAlert> {
|
||||
private func activeUser() -> Result<User, ErrorAlert> {
|
||||
do {
|
||||
guard let user = try apiGetActiveUser() else {
|
||||
return .failure(
|
||||
@@ -263,6 +268,14 @@ class ShareModel: ObservableObject {
|
||||
)
|
||||
)
|
||||
}
|
||||
return .success(user)
|
||||
} catch {
|
||||
return .failure(ErrorAlert(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchChats(_ user: User) -> Result<Array<SEChatData>, ErrorAlert> {
|
||||
do {
|
||||
return .success(try apiGetChats(userId: user.id))
|
||||
} catch {
|
||||
return .failure(ErrorAlert(error))
|
||||
@@ -405,7 +418,7 @@ enum SharedContent {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result<SharedContent, ErrorAlert> {
|
||||
fileprivate func getSharedContent(_ ip: NSItemProvider, _ senderProfile: LocalProfile) async -> Result<SharedContent, ErrorAlert> {
|
||||
if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) {
|
||||
switch type {
|
||||
// Prepare Image message
|
||||
@@ -443,15 +456,13 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result<SharedCo
|
||||
// Prepare Data message
|
||||
case .fileURL:
|
||||
if let url = try? await inPlaceUrl(type: .data) {
|
||||
if isFileTooLarge(for: url) {
|
||||
let sizeString = ByteCountFormatter.string(
|
||||
fromByteCount: Int64(getMaxFileSize(.xftp)),
|
||||
countStyle: .binary
|
||||
)
|
||||
let size = Int64(fileSize(url) ?? 0)
|
||||
let maxSize = getMaxFileSize(.xftp, senderProfile)
|
||||
if size > maxSize {
|
||||
return .failure(
|
||||
ErrorAlert(
|
||||
title: "Large file!",
|
||||
message: "Currently maximum supported file size is \(sizeString)."
|
||||
message: LocalizedStringKey(largeFileMessage(size, badgeIssue: expiredBadgeReason(size, senderProfile)))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -534,9 +545,3 @@ fileprivate func transcodeVideo(from input: URL) async -> URL? {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func isFileTooLarge(for url: URL) -> Bool {
|
||||
fileSize(url)
|
||||
.map { $0 > getMaxFileSize(.xftp) }
|
||||
?? false
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
|
||||
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; };
|
||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
|
||||
644CC229305BFAE400D2A571 /* BadgesLedgerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644CC228305BFAE400D2A571 /* BadgesLedgerView.swift */; };
|
||||
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
|
||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||
@@ -164,6 +165,7 @@
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
647B15E82F4C8D2500EB431E /* AddChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647B15E72F4C8D2500EB431E /* AddChannelView.swift */; };
|
||||
647B15EA2F4C8D5100EB431E /* ChatRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647B15E92F4C8D5100EB431E /* ChatRelayView.swift */; };
|
||||
647C9E293061362A0032110E /* GetStakeBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C9E283061362A0032110E /* GetStakeBanner.swift */; };
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||
@@ -179,12 +181,20 @@
|
||||
64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64C03BF9302F423300072BDE /* BadgesPayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF1302F423300072BDE /* BadgesPayView.swift */; };
|
||||
64C03BFA302F423300072BDE /* BadgesRedeemCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF2302F423300072BDE /* BadgesRedeemCodeView.swift */; };
|
||||
64C03BFB302F423300072BDE /* BadgesHowItWorksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF0302F423300072BDE /* BadgesHowItWorksView.swift */; };
|
||||
64C03BFC302F423300072BDE /* BadgeUserPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF6302F423300072BDE /* BadgeUserPreview.swift */; };
|
||||
64C03BFD302F423300072BDE /* BadgeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF4302F423300072BDE /* BadgeStore.swift */; };
|
||||
64C03BFE302F423300072BDE /* SupportSimpleXBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF7302F423300072BDE /* SupportSimpleXBanner.swift */; };
|
||||
64C03BFF302F423300072BDE /* BadgesYourLevelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF5302F423300072BDE /* BadgesYourLevelView.swift */; };
|
||||
64C03C00302F423300072BDE /* BadgesSupportSimplexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C03BF3302F423300072BDE /* BadgesSupportSimplexView.swift */; };
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */; };
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
@@ -192,6 +202,8 @@
|
||||
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; };
|
||||
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */; };
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64EB8A99305405C60089FFDF /* BadgesYourBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EB8A98305405C60089FFDF /* BadgesYourBadgeView.swift */; };
|
||||
64EB8A9B3054347A0089FFDF /* BadgesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EB8A9A3054347A0089FFDF /* BadgesView.swift */; };
|
||||
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */; };
|
||||
@@ -526,6 +538,7 @@
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
||||
642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = "<group>"; };
|
||||
642E412B3033084000F04EE1 /* BadgeTestProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeTestProducts.storekit; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
@@ -534,6 +547,7 @@
|
||||
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
|
||||
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
|
||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
|
||||
644CC228305BFAE400D2A571 /* BadgesLedgerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesLedgerView.swift; sourceTree = "<group>"; };
|
||||
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||
@@ -543,6 +557,7 @@
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
647B15E72F4C8D2500EB431E /* AddChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddChannelView.swift; sourceTree = "<group>"; };
|
||||
647B15E92F4C8D5100EB431E /* ChatRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRelayView.swift; sourceTree = "<group>"; };
|
||||
647C9E283061362A0032110E /* GetStakeBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStakeBanner.swift; sourceTree = "<group>"; };
|
||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
||||
648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; };
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
||||
@@ -559,12 +574,20 @@
|
||||
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextPendingMemberActionsView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64C03BF0302F423300072BDE /* BadgesHowItWorksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesHowItWorksView.swift; sourceTree = "<group>"; };
|
||||
64C03BF1302F423300072BDE /* BadgesPayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesPayView.swift; sourceTree = "<group>"; };
|
||||
64C03BF2302F423300072BDE /* BadgesRedeemCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesRedeemCodeView.swift; sourceTree = "<group>"; };
|
||||
64C03BF3302F423300072BDE /* BadgesSupportSimplexView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesSupportSimplexView.swift; sourceTree = "<group>"; };
|
||||
64C03BF4302F423300072BDE /* BadgeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeStore.swift; sourceTree = "<group>"; };
|
||||
64C03BF5302F423300072BDE /* BadgesYourLevelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesYourLevelView.swift; sourceTree = "<group>"; };
|
||||
64C03BF6302F423300072BDE /* BadgeUserPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeUserPreview.swift; sourceTree = "<group>"; };
|
||||
64C03BF7302F423300072BDE /* SupportSimpleXBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportSimpleXBanner.swift; sourceTree = "<group>"; };
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -573,6 +596,8 @@
|
||||
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = "<group>"; };
|
||||
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextProfilePickerView.swift; sourceTree = "<group>"; };
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64EB8A98305405C60089FFDF /* BadgesYourBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesYourBadgeView.swift; sourceTree = "<group>"; };
|
||||
64EB8A9A3054347A0089FFDF /* BadgesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgesView.swift; sourceTree = "<group>"; };
|
||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMemberContactActionsView.swift; sourceTree = "<group>"; };
|
||||
@@ -735,8 +760,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -772,6 +797,7 @@
|
||||
5C2E260D27A30E2400F70299 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64C03BF8302F423300072BDE /* Badges */,
|
||||
B76E6C2F2C5C41C300EC11AA /* Contacts */,
|
||||
5CB0BA8C282711BC00B3292C /* Onboarding */,
|
||||
3C714775281C080100CB4D4B /* Call */,
|
||||
@@ -822,8 +848,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.4-JuaCEFy75SxELqTayb7fbQ.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -892,6 +918,7 @@
|
||||
5CA059BD279559F40002BEB4 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
642E412B3033084000F04EE1 /* BadgeTestProducts.storekit */,
|
||||
E5C0BBFD2F82BBC000EA7527 /* Debug.xcconfig */,
|
||||
E5C0BBFE2F82BBC900EA7527 /* Release.xcconfig */,
|
||||
5C55A92D283D0FDE00C4E99E /* sounds */,
|
||||
@@ -1027,6 +1054,7 @@
|
||||
5CB9250B27A942F300ACCCDD /* ChatList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
647C9E283061362A0032110E /* GetStakeBanner.swift */,
|
||||
5C2E260A27A30CFA00F70299 /* ChatListView.swift */,
|
||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */,
|
||||
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
|
||||
@@ -1186,6 +1214,24 @@
|
||||
path = Group;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
64C03BF8302F423300072BDE /* Badges */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
644CC228305BFAE400D2A571 /* BadgesLedgerView.swift */,
|
||||
64EB8A9A3054347A0089FFDF /* BadgesView.swift */,
|
||||
64C03BF0302F423300072BDE /* BadgesHowItWorksView.swift */,
|
||||
64C03BF1302F423300072BDE /* BadgesPayView.swift */,
|
||||
64C03BF2302F423300072BDE /* BadgesRedeemCodeView.swift */,
|
||||
64C03BF3302F423300072BDE /* BadgesSupportSimplexView.swift */,
|
||||
64C03BF4302F423300072BDE /* BadgeStore.swift */,
|
||||
64C03BF5302F423300072BDE /* BadgesYourLevelView.swift */,
|
||||
64C03BF6302F423300072BDE /* BadgeUserPreview.swift */,
|
||||
64C03BF7302F423300072BDE /* SupportSimpleXBanner.swift */,
|
||||
64EB8A98305405C60089FFDF /* BadgesYourBadgeView.swift */,
|
||||
);
|
||||
path = Badges;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8C74C3E92C1B909200039E77 /* Theme */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1537,6 +1583,7 @@
|
||||
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */,
|
||||
644CC229305BFAE400D2A571 /* BadgesLedgerView.swift in Sources */,
|
||||
647B15EA2F4C8D5100EB431E /* ChatRelayView.swift in Sources */,
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
|
||||
8CC317462D4FEBA800292A20 /* ScrollViewCells.swift in Sources */,
|
||||
@@ -1602,6 +1649,14 @@
|
||||
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */,
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
|
||||
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */,
|
||||
64C03BF9302F423300072BDE /* BadgesPayView.swift in Sources */,
|
||||
64C03BFA302F423300072BDE /* BadgesRedeemCodeView.swift in Sources */,
|
||||
64C03BFB302F423300072BDE /* BadgesHowItWorksView.swift in Sources */,
|
||||
64C03BFC302F423300072BDE /* BadgeUserPreview.swift in Sources */,
|
||||
64C03BFD302F423300072BDE /* BadgeStore.swift in Sources */,
|
||||
64C03BFE302F423300072BDE /* SupportSimpleXBanner.swift in Sources */,
|
||||
64C03BFF302F423300072BDE /* BadgesYourLevelView.swift in Sources */,
|
||||
64C03C00302F423300072BDE /* BadgesSupportSimplexView.swift in Sources */,
|
||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
|
||||
5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */,
|
||||
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
|
||||
@@ -1643,6 +1698,7 @@
|
||||
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */,
|
||||
647B15E82F4C8D2500EB431E /* AddChannelView.swift in Sources */,
|
||||
6495D7062F48CFFD0060512B /* ChannelRelaysView.swift in Sources */,
|
||||
64EB8A9B3054347A0089FFDF /* BadgesView.swift in Sources */,
|
||||
E5E418022F83D2CA00252B9E /* ChannelWebAccessView.swift in Sources */,
|
||||
6495D7082F48D0000060512B /* AddGroupRelayView.swift in Sources */,
|
||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
|
||||
@@ -1683,12 +1739,14 @@
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */,
|
||||
647C9E293061362A0032110E /* GetStakeBanner.swift in Sources */,
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */,
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */,
|
||||
6495D7042F48CFC50060512B /* ChannelMembersView.swift in Sources */,
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */,
|
||||
64EB8A99305405C60089FFDF /* BadgesYourBadgeView.swift in Sources */,
|
||||
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */,
|
||||
1841538E296606C74533367C /* UserPicker.swift in Sources */,
|
||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */,
|
||||
@@ -2081,7 +2139,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2131,7 +2189,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2173,7 +2231,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2193,7 +2251,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2218,7 +2276,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2255,7 +2313,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2292,7 +2350,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2343,7 +2401,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2397,7 +2455,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2431,7 +2489,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 352;
|
||||
CURRENT_PROJECT_VERSION = 357;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
||||
@@ -163,6 +163,14 @@ public func chatJsonLength(_ s: String) -> Int {
|
||||
return Int(chat_json_length(&c))
|
||||
}
|
||||
|
||||
// the code in core's canonical form (prefix and 20 characters, no separators), or nil when core rejects it.
|
||||
// The check character is verified by core via FFI, not re-implemented here, so the two cannot disagree.
|
||||
public func parseBadgeCode(_ s: String) -> String? {
|
||||
var c = s.cString(using: .utf8)!
|
||||
let canonical = fromCString(chat_parse_badge_code(&c)!)
|
||||
return canonical.isEmpty ? nil : canonical
|
||||
}
|
||||
|
||||
struct ParsedMarkdown: Decodable {
|
||||
var formattedText: [FormattedText]?
|
||||
}
|
||||
|
||||
@@ -812,6 +812,7 @@ public enum ChatErrorType: Decodable, Hashable {
|
||||
case agentVersion
|
||||
case agentNoSubResult(agentConnId: String)
|
||||
case commandError(message: String)
|
||||
case badgeRedeemError(badgeRedeemError: BadgeRedeemError)
|
||||
case serverProtocol
|
||||
case agentCommandError(message: String)
|
||||
case invalidFileDescription(message: String)
|
||||
@@ -823,6 +824,84 @@ public enum ChatErrorType: Decodable, Hashable {
|
||||
case exception(message: String)
|
||||
}
|
||||
|
||||
public enum BadgeRedeemError: Decodable, Hashable {
|
||||
case invalidCode
|
||||
case serviceNotConfigured
|
||||
case badgeActive
|
||||
case serviceError(serviceError: BadgeServiceErrorCode)
|
||||
case invalidResponse(message: String)
|
||||
case unknownKeyIndex
|
||||
case credentialNotVerified
|
||||
}
|
||||
|
||||
// the service is deployed ahead of clients, so a code this version does not know keeps its tag
|
||||
public enum BadgeServiceErrorCode: Decodable, Hashable {
|
||||
case badRequest
|
||||
case unsupportedVersion
|
||||
case unknownPurchaseKey
|
||||
case unknownOfferId
|
||||
case offerDisabled
|
||||
case offerMismatch
|
||||
case productUnavailable
|
||||
case paymentNotEntitled
|
||||
case paymentPending
|
||||
case providerUnavailable
|
||||
case rateLimited
|
||||
case codeInvalid
|
||||
case codeUsed
|
||||
case codeExpired
|
||||
case receiptInvalid
|
||||
case receiptUsed
|
||||
case internalError
|
||||
case unknown(String)
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .badRequest: "bad_request"
|
||||
case .unsupportedVersion: "unsupported_version"
|
||||
case .unknownPurchaseKey: "unknown_purchase_key"
|
||||
case .unknownOfferId: "unknown_offer_id"
|
||||
case .offerDisabled: "offer_disabled"
|
||||
case .offerMismatch: "offer_mismatch"
|
||||
case .productUnavailable: "product_unavailable"
|
||||
case .paymentNotEntitled: "payment_not_entitled"
|
||||
case .paymentPending: "payment_pending"
|
||||
case .providerUnavailable: "provider_unavailable"
|
||||
case .rateLimited: "rate_limited"
|
||||
case .codeInvalid: "code_invalid"
|
||||
case .codeUsed: "code_used"
|
||||
case .codeExpired: "code_expired"
|
||||
case .receiptInvalid: "receipt_invalid"
|
||||
case .receiptUsed: "receipt_used"
|
||||
case .internalError: "internal"
|
||||
case let .unknown(s): s
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
switch try decoder.singleValueContainer().decode(String.self) {
|
||||
case "bad_request": self = .badRequest
|
||||
case "unsupported_version": self = .unsupportedVersion
|
||||
case "unknown_purchase_key": self = .unknownPurchaseKey
|
||||
case "unknown_offer_id": self = .unknownOfferId
|
||||
case "offer_disabled": self = .offerDisabled
|
||||
case "offer_mismatch": self = .offerMismatch
|
||||
case "product_unavailable": self = .productUnavailable
|
||||
case "payment_not_entitled": self = .paymentNotEntitled
|
||||
case "payment_pending": self = .paymentPending
|
||||
case "provider_unavailable": self = .providerUnavailable
|
||||
case "rate_limited": self = .rateLimited
|
||||
case "code_invalid": self = .codeInvalid
|
||||
case "code_used": self = .codeUsed
|
||||
case "code_expired": self = .codeExpired
|
||||
case "receipt_invalid": self = .receiptInvalid
|
||||
case "receipt_used": self = .receiptUsed
|
||||
case "internal": self = .internalError
|
||||
case let s: self = .unknown(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StoreError: Decodable, Hashable {
|
||||
case duplicateName
|
||||
case userNotFound(userId: Int64)
|
||||
@@ -1077,6 +1156,15 @@ public enum SMPAgentError: Decodable, Hashable {
|
||||
case A_CRYPTO
|
||||
case A_DUPLICATE
|
||||
case A_QUEUE(queueErr: String)
|
||||
case A_SERVICE(serviceError: AgentServiceError)
|
||||
}
|
||||
|
||||
public enum AgentServiceError: Decodable, Hashable {
|
||||
case rejected
|
||||
case timeout
|
||||
case noPendingRequest
|
||||
case notDRAddress
|
||||
case badSignature
|
||||
}
|
||||
|
||||
public enum ArchiveError: Decodable, Hashable {
|
||||
|
||||
@@ -304,11 +304,221 @@ public struct BadgeInfo: Codable, Hashable {
|
||||
public var badgeType: BadgeType
|
||||
public var badgeExpiry: Date
|
||||
public var badgeExtra: String
|
||||
|
||||
public init(badgeType: BadgeType, badgeExpiry: Date, badgeExtra: String = "") {
|
||||
self.badgeType = badgeType
|
||||
self.badgeExpiry = badgeExpiry
|
||||
self.badgeExtra = badgeExtra
|
||||
}
|
||||
}
|
||||
|
||||
public struct LocalBadge: Codable, Hashable {
|
||||
public var badge: BadgeInfo
|
||||
public var status: BadgeStatus
|
||||
|
||||
public init(badge: BadgeInfo, status: BadgeStatus) {
|
||||
self.badge = badge
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
||||
// paidThrough is the only date to show the user: BadgeInfo.badgeExpiry is the credential's expiry,
|
||||
// which outlives entitlement so the credential's window can cover a renewal.
|
||||
public struct BadgeState: Codable, Hashable {
|
||||
public var badgePurchaseId: Int64
|
||||
public var purchaseKey: String
|
||||
public var badgeType: BadgeType
|
||||
public var shown: Bool
|
||||
public var monthsLeft: Int
|
||||
public var paidThrough: Date
|
||||
public var renewsAt: Date?
|
||||
public var willRenew: Bool
|
||||
public var alert: BadgeAlert?
|
||||
|
||||
public var paidThroughText: String { badgeDateText(paidThrough) }
|
||||
}
|
||||
|
||||
public struct StatementEntry: Codable, Hashable {
|
||||
public var entryId: String
|
||||
public var changeMonths: Int
|
||||
public var balanceMonths: Int
|
||||
public var balanceStartTs: Date
|
||||
public var balanceAnchorTs: Date
|
||||
public var balanceBadgeType: BadgeType
|
||||
public var wasPausedSince: Date?
|
||||
public var createdAt: Date
|
||||
public var entryType: StatementEntryType
|
||||
}
|
||||
|
||||
public enum StatementEntryType: Codable, Hashable {
|
||||
case credit(StatementCreditType)
|
||||
case debit(StatementDebitType)
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case credit
|
||||
case debit
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
switch type {
|
||||
case "credit": self = .credit(try container.decode(StatementCreditType.self, forKey: .credit))
|
||||
case "debit": self = .debit(try container.decode(StatementDebitType.self, forKey: .debit))
|
||||
default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "unknown entry type \(type)")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .credit(c):
|
||||
try container.encode("credit", forKey: .type)
|
||||
try container.encode(c, forKey: .credit)
|
||||
case let .debit(d):
|
||||
try container.encode("debit", forKey: .type)
|
||||
try container.encode(d, forKey: .debit)
|
||||
}
|
||||
}
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case let .credit(c): c.text
|
||||
case let .debit(d): d.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the service is deployed ahead of clients, so a type this version does not know keeps its tag
|
||||
public enum StatementCreditType: Codable, Hashable {
|
||||
case payment(invoiceId: String?)
|
||||
case code
|
||||
case charge(chargeId: String)
|
||||
case support
|
||||
case transferIn(fromPurchaseKey: String)
|
||||
case opening
|
||||
case unknown(type: String)
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case invoiceId
|
||||
case chargeId
|
||||
case fromPurchaseKey
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
switch type {
|
||||
case "payment": self = .payment(invoiceId: try container.decodeIfPresent(String.self, forKey: .invoiceId))
|
||||
case "code": self = .code
|
||||
case "charge": self = .charge(chargeId: try container.decode(String.self, forKey: .chargeId))
|
||||
case "support": self = .support
|
||||
case "transferIn": self = .transferIn(fromPurchaseKey: try container.decode(String.self, forKey: .fromPurchaseKey))
|
||||
case "opening": self = .opening
|
||||
default: self = .unknown(type: type)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(text, forKey: .type)
|
||||
switch self {
|
||||
case let .payment(invoiceId): try container.encodeIfPresent(invoiceId, forKey: .invoiceId)
|
||||
case let .charge(chargeId): try container.encode(chargeId, forKey: .chargeId)
|
||||
case let .transferIn(fromPurchaseKey): try container.encode(fromPurchaseKey, forKey: .fromPurchaseKey)
|
||||
case .code, .support, .opening, .unknown: ()
|
||||
}
|
||||
}
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .payment: "payment"
|
||||
case .code: "code"
|
||||
case .charge: "charge"
|
||||
case .support: "support"
|
||||
case .transferIn: "transferIn"
|
||||
case .opening: "opening"
|
||||
case let .unknown(type): type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StatementDebitType: Codable, Hashable {
|
||||
case refund
|
||||
case upgrade(toPurchaseKey: String)
|
||||
case transferOut(toPurchaseKey: String)
|
||||
case support
|
||||
case badge
|
||||
case lapse
|
||||
case unknown(type: String)
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case toPurchaseKey
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
switch type {
|
||||
case "refund": self = .refund
|
||||
case "upgrade": self = .upgrade(toPurchaseKey: try container.decode(String.self, forKey: .toPurchaseKey))
|
||||
case "transferOut": self = .transferOut(toPurchaseKey: try container.decode(String.self, forKey: .toPurchaseKey))
|
||||
case "support": self = .support
|
||||
case "badge": self = .badge
|
||||
case "lapse": self = .lapse
|
||||
default: self = .unknown(type: type)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(text, forKey: .type)
|
||||
switch self {
|
||||
case let .upgrade(toPurchaseKey), let .transferOut(toPurchaseKey): try container.encode(toPurchaseKey, forKey: .toPurchaseKey)
|
||||
case .refund, .support, .badge, .lapse, .unknown: ()
|
||||
}
|
||||
}
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .refund: "refund"
|
||||
case .upgrade: "upgrade"
|
||||
case .transferOut: "transferOut"
|
||||
case .support: "support"
|
||||
case .badge: "badge"
|
||||
case .lapse: "lapse"
|
||||
case let .unknown(type): type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct BadgeAlert: Codable, Hashable {
|
||||
public var kind: BadgeAlertKind
|
||||
public var episode: String
|
||||
public var date: Date
|
||||
public var price: BadgeAlertPrice?
|
||||
|
||||
public var dateText: String { badgeDateText(date) }
|
||||
}
|
||||
|
||||
private func badgeDateText(_ date: Date) -> String {
|
||||
DateFormatter.localizedString(from: date, dateStyle: .long, timeStyle: .none)
|
||||
}
|
||||
|
||||
public struct BadgeAlertPrice: Codable, Hashable {
|
||||
public var amount: Int64
|
||||
public var currency: String
|
||||
}
|
||||
|
||||
public enum BadgeAlertKind: String, Codable, Hashable {
|
||||
case renewalApproaching
|
||||
case paymentIssue
|
||||
case subscriptionEnded
|
||||
case prepaidEnding
|
||||
case supportEnded
|
||||
}
|
||||
|
||||
// the wire proof carried on a profile - opaque to the UI, only round-tripped back to the core (apiPrepareContact)
|
||||
@@ -4678,6 +4888,17 @@ extension MsgReaction: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
// set by the core when the file is above the size the sender's badge allows; badgeStatus is nil when no proof was sent
|
||||
public struct FileProhibited: Decodable, Hashable {
|
||||
public var maxSize: Int64
|
||||
public var badgeStatus: BadgeStatus?
|
||||
|
||||
public init(maxSize: Int64, badgeStatus: BadgeStatus?) {
|
||||
self.maxSize = maxSize
|
||||
self.badgeStatus = badgeStatus
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIFile: Decodable, Hashable {
|
||||
public var fileId: Int64
|
||||
public var fileName: String
|
||||
@@ -4686,6 +4907,7 @@ public struct CIFile: Decodable, Hashable {
|
||||
public var fileStatus: CIFileStatus
|
||||
public var fileProtocol: FileProtocol
|
||||
public var fileExpires: Date? = nil
|
||||
public var fileProhibited: FileProhibited? = nil
|
||||
|
||||
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
let f: CryptoFile?
|
||||
|
||||
@@ -29,10 +29,13 @@ public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB
|
||||
// Spec: spec/services/files.md#MAX_FILE_SIZE_XFTP
|
||||
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB
|
||||
|
||||
// raised XFTP receive limits for files from a sender with a supporter badge (also investor) or a legend badge
|
||||
// raised XFTP limits for a user with a supporter badge (also investor) or a legend badge
|
||||
public let MAX_FILE_SIZE_XFTP_SUPPORTER: Int64 = 2_147_483_648 // 2GB
|
||||
public let MAX_FILE_SIZE_XFTP_LEGEND: Int64 = 5_368_709_120 // 5GB
|
||||
|
||||
// a badge raises the limit at send for this long after its expiry, shorter than the receiver's grace
|
||||
public let BADGE_SND_GRACE_INTERVAL = TimeInterval(86400)
|
||||
|
||||
public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max
|
||||
|
||||
public let MAX_FILE_SIZE_SMP: Int64 = 8000000
|
||||
@@ -277,29 +280,63 @@ public func cleanupFile(_ aChatItem: AChatItem) {
|
||||
}
|
||||
}
|
||||
|
||||
public func badgeMaxFileSize(_ badge: LocalBadge) -> Int64 {
|
||||
badge.badge.badgeType == .legend ? MAX_FILE_SIZE_XFTP_LEGEND : MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
}
|
||||
|
||||
// a badge raises the limit at send until one day past its expiry, as the core applies it
|
||||
public func badgeActiveForSend(_ badge: LocalBadge) -> Bool {
|
||||
badge.status == .active && badge.badge.badgeExpiry.addingTimeInterval(BADGE_SND_GRACE_INTERVAL) >= Date.now
|
||||
}
|
||||
|
||||
// in incognito chats and above the largest badge's limit no badge applies, so badgeIssue is not used
|
||||
public func largeFileMessage(_ fileSize: Int64, incognito: Bool = false, badgeIssue: String = "") -> String {
|
||||
if incognito {
|
||||
return String.localizedStringWithFormat(
|
||||
NSLocalizedString("Files larger than %@ cannot be sent in incognito chats.", comment: "file alert"),
|
||||
ByteCountFormatter.string(fromByteCount: MAX_FILE_SIZE_XFTP, countStyle: .binary)
|
||||
)
|
||||
}
|
||||
if fileSize > MAX_FILE_SIZE_XFTP_LEGEND {
|
||||
return String.localizedStringWithFormat(
|
||||
NSLocalizedString("Maximum supported file size is %1$@, with a %2$@.", comment: "file alert"),
|
||||
ByteCountFormatter.string(fromByteCount: MAX_FILE_SIZE_XFTP_LEGEND, countStyle: .binary),
|
||||
NSLocalizedString("legend badge", comment: "badge required to send a large file")
|
||||
)
|
||||
}
|
||||
let supporter = fileSize <= MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
let message = String.localizedStringWithFormat(
|
||||
NSLocalizedString("You need a %1$@ to send files larger than %2$@.", comment: "file alert"),
|
||||
supporter
|
||||
? NSLocalizedString("supporter badge", comment: "badge required to send a large file")
|
||||
: NSLocalizedString("legend badge", comment: "badge required to send a large file"),
|
||||
ByteCountFormatter.string(fromByteCount: supporter ? MAX_FILE_SIZE_XFTP : MAX_FILE_SIZE_XFTP_SUPPORTER, countStyle: .binary)
|
||||
)
|
||||
return badgeIssue.isEmpty ? message : message + " " + badgeIssue
|
||||
}
|
||||
|
||||
// the badge lapsed, and while active it would have allowed this file
|
||||
public func expiredBadgeReason(_ fileSize: Int64, _ senderProfile: LocalProfile?) -> String {
|
||||
if let badge = senderProfile?.localBadge, !badgeActiveForSend(badge), badgeMaxFileSize(badge) >= fileSize {
|
||||
NSLocalizedString("Your badge expired.", comment: "file alert")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
public func getMaxFileSize(_ fileProtocol: FileProtocol, _ senderProfile: LocalProfile? = nil) -> Int64 {
|
||||
switch fileProtocol {
|
||||
case .smp: MAX_FILE_SIZE_SMP
|
||||
case .local: MAX_FILE_SIZE_LOCAL
|
||||
// a sender's active badge raises the XFTP limit: legend to 5GB, any other (supporter/investor) to 2GB
|
||||
case .xftp:
|
||||
if let badge = senderProfile?.localBadge, badge.status == .active {
|
||||
badge.badge.badgeType == .legend ? MAX_FILE_SIZE_XFTP_LEGEND : MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
if let badge = senderProfile?.localBadge, badgeActiveForSend(badge) {
|
||||
badgeMaxFileSize(badge)
|
||||
} else {
|
||||
MAX_FILE_SIZE_XFTP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the profile of whoever sent a received chat item - the group member, or the direct chat's contact
|
||||
public func ciSenderProfile(_ ci: ChatItem, _ chatInfo: ChatInfo) -> LocalProfile? {
|
||||
switch (ci.chatDir, chatInfo) {
|
||||
case let (.groupRcv(groupMember), _): return groupMember.memberProfile
|
||||
case let (.directRcv, .direct(contact)): return contact.profile
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public struct RuntimeError: Error {
|
||||
let message: String
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ extern char *chat_parse_server(char *str);
|
||||
extern char *chat_parse_uri(char *str, int safe);
|
||||
extern char *chat_password_hash(char *pwd, char *salt);
|
||||
extern char *chat_valid_name(char *name);
|
||||
// returns the canonical form of a badge code, or an empty string if it does not parse
|
||||
extern char *chat_parse_badge_code(char *code);
|
||||
extern int chat_json_length(char *str);
|
||||
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||
|
||||
@@ -19,7 +19,7 @@ This document provides a structured mapping between product-level concepts, thei
|
||||
|
||||
| # | Concept | Product Docs | Spec Docs | Source Files (Swift) | Source Files (Haskell) |
|
||||
|---|---------|-------------|-----------|---------------------|----------------------|
|
||||
| 1 | Chat List | [views/chat-list.md](views/chat-list.md), [views/onboarding.md](views/onboarding.md) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `Shared/Views/ChatList/ChatListView.swift` | `Controller.hs` (`APIGetChats`) |
|
||||
| 1 | Chat List | [views/chat-list.md](views/chat-list.md), [views/onboarding.md](views/onboarding.md) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `Shared/Views/ChatList/ChatListView.swift`, `Shared/Views/ChatList/GetStakeBanner.swift` | `Controller.hs` (`APIGetChats`) |
|
||||
| 2 | Direct Chat | [views/chat.md](views/chat.md), [flows/messaging.md](flows/messaging.md) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `Shared/Views/Chat/ChatView.swift`, `ChatInfoView.swift` | `Types.hs` (`Contact`), `Messages.hs` |
|
||||
| 3 | Group Chat | [views/chat.md](views/chat.md), [views/group-info.md](views/group-info.md) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `Shared/Views/Chat/ChatView.swift`, `Group/GroupChatInfoView.swift` | `Types.hs` (`GroupInfo`, `GroupMember`) |
|
||||
| 4 | Message Composition | [views/chat.md](views/chat.md) | [spec/client/compose.md](../spec/client/compose.md) | `ComposeMessage/ComposeView.swift`, `SendMessageView.swift` | `Controller.hs` (`APISendMessages`) |
|
||||
|
||||
@@ -93,6 +93,7 @@ When a relay address link (`/r` path) is opened via URL deep link, `ContentView.
|
||||
|
||||
- **One-hand UI card** (`OneHandUICard`): Dismissible card shown to introduce bottom toolbar mode
|
||||
- **Address creation card** (`AddressCreationCard`): Prompts user to create a SimpleX address
|
||||
- **Crowdfunding banner** (`GetStakeBanner`): Shown to users in the US only, above the chats and below the onboarding pages. Tapping it opens `GetStakeView` (invest on Wefunder). The dismiss X appears once the banner has been tapped and there is at least one chat; dismissing hides it until hints are reset
|
||||
|
||||
### Pull-to-Refresh
|
||||
|
||||
@@ -103,7 +104,7 @@ Triggers `reconnectAllServers()` after user confirmation alert ("Reconnect serve
|
||||
| State | Behavior |
|
||||
|---|---|
|
||||
| Chat database not started | Settings row shows exclamation icon; chat running == false disables interactions |
|
||||
| No chats | `ChatHelp` view displayed with onboarding guidance |
|
||||
| No conversations yet | `ConnectOnboardingView` pages replace the list, with the crowdfunding banner below them where it applies |
|
||||
| Connection in progress | `ConnectProgressManager` overlay with connecting text |
|
||||
| Search with no results | Empty list with no special empty-state view |
|
||||
|
||||
@@ -127,4 +128,5 @@ Triggers `reconnectAllServers()` after user confirmation alert ("Reconnect serve
|
||||
- `Shared/Views/ChatList/ContactRequestView.swift` -- Contact request row rendering
|
||||
- `Shared/Views/ChatList/ContactConnectionView.swift` -- Pending connection row rendering
|
||||
- `Shared/Views/ChatList/OneHandUICard.swift` -- One-hand UI introduction card
|
||||
- `Shared/Views/ChatList/GetStakeBanner.swift` -- Wefunder crowdfunding banner and shared banner card chrome
|
||||
- `Shared/Views/ChatList/ServersSummaryView.swift` -- Server subscription summary
|
||||
|
||||
@@ -534,7 +534,8 @@ func chatApiSendCmdWithRetry<R: ChatAPIResult>( // SimpleXAPI.swift L127
|
||||
bgTask: Bool = true,
|
||||
bgDelay: Double? = nil,
|
||||
inProgress: BoxedValue<Bool>? = nil,
|
||||
retryNum: Int32 = 0
|
||||
retryNum: Int32 = 0,
|
||||
log: Bool = true
|
||||
) async -> APIResult<R>?
|
||||
```
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
7. [Swipe Actions](#7-swipe-actions)
|
||||
8. [UserPicker](#8-userpicker)
|
||||
9. [Floating Action Button](#9-floating-action-button)
|
||||
10. [Crowdfunding Banner](#10-crowdfunding-banner)
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +54,7 @@ ChatListView
|
||||
|
||||
---
|
||||
|
||||
## 2. [`ChatListView`](../../Shared/Views/ChatList/ChatListView.swift#L142) {#2-chatlistview}
|
||||
## 2. [`ChatListView`](../../Shared/Views/ChatList/ChatListView.swift#L154) {#2-chatlistview}
|
||||
|
||||
**File**: `Shared/Views/ChatList/ChatListView.swift`
|
||||
|
||||
@@ -62,7 +63,7 @@ The root list view. Key responsibilities:
|
||||
### Data Source
|
||||
- Reads `ChatModel.shared.chats` (all conversations)
|
||||
- Applies active filter from `ChatTagsModel.shared.activeFilter`
|
||||
- Applies search query filtering via [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480)
|
||||
- Applies search query filtering via [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L555)
|
||||
- Sorts by last activity (most recent first), with pinned chats at top
|
||||
|
||||
### Layout
|
||||
@@ -79,11 +80,11 @@ The root list view. Key responsibilities:
|
||||
|
||||
| Function | Line | Description |
|
||||
|----------|------|-------------|
|
||||
| [`body`](../../Shared/Views/ChatList/ChatListView.swift#L168) | 163 | Main view body |
|
||||
| [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) | 472 | Applies active filter and search to chat list |
|
||||
| [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) | 514 | Normalizes search text for comparison |
|
||||
| [`unreadBadge()`](../../Shared/Views/ChatList/ChatListView.swift#L454) | 448 | Renders unread count circle badge |
|
||||
| [`stopAudioPlayer()`](../../Shared/Views/ChatList/ChatListView.swift#L474) | 467 | Stops any playing voice message |
|
||||
| [`body`](../../Shared/Views/ChatList/ChatListView.swift#L183) | 183 | Main view body |
|
||||
| [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L555) | 555 | Applies active filter and search to chat list |
|
||||
| [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L598) | 598 | Normalizes search text for comparison |
|
||||
| [`unreadBadge()`](../../Shared/Views/ChatList/ChatListView.swift#L529) | 529 | Renders unread count circle badge |
|
||||
| [`stopAudioPlayer()`](../../Shared/Views/ChatList/ChatListView.swift#L549) | 549 | Stops any playing voice message |
|
||||
|
||||
---
|
||||
|
||||
@@ -171,7 +172,7 @@ Horizontal scrolling tab bar below the navigation bar. Tabs:
|
||||
| Group Reports | `.presetTag(.groupReports)` | Groups with pending reports |
|
||||
| User tags | `.userTag(ChatTag)` | User-defined custom tags |
|
||||
|
||||
Filter matching is handled by [`presetTagMatchesChat()`](../../Shared/Views/ChatList/ChatListView.swift#L910) (L910) and the in-view [`TagsView`](../../Shared/Views/ChatList/ChatListView.swift#L705) struct (L705).
|
||||
Filter matching is handled by [`presetTagMatchesChat()`](../../Shared/Views/ChatList/ChatListView.swift#L1134) and the in-view [`TagsView`](../../Shared/Views/ChatList/ChatListView.swift#L928) struct.
|
||||
|
||||
### ChatTagsModel State
|
||||
|
||||
@@ -194,9 +195,9 @@ class ChatTagsModel: ObservableObject {
|
||||
|
||||
| Type | File | Line | Description |
|
||||
|------|------|------|-------------|
|
||||
| [`PresetTag`](../../Shared/Views/ChatList/ChatListView.swift#L36) | ChatListView.swift | 34 | Enum of built-in filter categories |
|
||||
| [`ActiveFilter`](../../Shared/Views/ChatList/ChatListView.swift#L52) | ChatListView.swift | 49 | Enum wrapping preset, user-tag, or unread filter |
|
||||
| [`setActiveFilter()`](../../Shared/Views/ChatList/ChatListView.swift#L889) | ChatListView.swift | 878 | Applies a filter and persists selection |
|
||||
| [`PresetTag`](../../Shared/Views/ChatList/ChatListView.swift#L36) | ChatListView.swift | 36 | Enum of built-in filter categories |
|
||||
| [`ActiveFilter`](../../Shared/Views/ChatList/ChatListView.swift#L53) | ChatListView.swift | 53 | Enum wrapping preset, user-tag, or unread filter |
|
||||
| [`setActiveFilter()`](../../Shared/Views/ChatList/ChatListView.swift#L1113) | ChatListView.swift | 1113 | Applies a filter and persists selection |
|
||||
|
||||
### Tag Management Commands
|
||||
- `apiCreateChatTag(tag: ChatTagData)` -- create tag
|
||||
@@ -211,7 +212,7 @@ class ChatTagsModel: ObservableObject {
|
||||
|
||||
Search is available via pull-down gesture or search button in the navigation bar.
|
||||
|
||||
**Search bar UI:** [`ChatListSearchBar`](../../Shared/Views/ChatList/ChatListView.swift#L587) (ChatListView.swift L578)
|
||||
**Search bar UI:** [`ChatListSearchBar`](../../Shared/Views/ChatList/ChatListView.swift#L662)
|
||||
|
||||
### Filtering Logic
|
||||
- Filters `ChatModel.chats` by matching search text against:
|
||||
@@ -219,7 +220,7 @@ Search is available via pull-down gesture or search button in the navigation bar
|
||||
- `chatInfo.localAlias` (local alias)
|
||||
- `chatInfo.fullName` (full name)
|
||||
- For deeper message content search, uses `apiGetChat(chatId:, search:)` parameter
|
||||
- Core logic in [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) (L480) and [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) (L523)
|
||||
- Core logic in [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L555) and [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L598)
|
||||
|
||||
### Search Results
|
||||
- Matching chats are displayed in the same list format
|
||||
@@ -279,11 +280,43 @@ The FAB (floating action button) in the bottom-right corner opens the new chat f
|
||||
|
||||
---
|
||||
|
||||
## 10. [`GetStakeBanner`](../../Shared/Views/ChatList/GetStakeBanner.swift#L13) {#10-crowdfunding-banner}
|
||||
|
||||
**File**: `Shared/Views/ChatList/GetStakeBanner.swift`
|
||||
|
||||
Gradient card inviting the user to invest on Wefunder. Shown only when [`isInUS`](../../Shared/Views/Onboarding/WhatsNewView.swift#L45) — the same condition that gates the Wefunder row in settings — and only while `DEFAULT_GET_STAKE_BANNER_DISMISSED` is false.
|
||||
|
||||
### Placement
|
||||
|
||||
| Where | Condition | Layout |
|
||||
|-------|-----------|--------|
|
||||
| Chat list | rendered whenever [`chatListContent`](../../Shared/Views/ChatList/ChatListView.swift#L413) is, in the `List` after `OneHandUICard` and before the chats | `.padding(.vertical, 3)`, flipped for one-hand UI, `.zIndex(1)` |
|
||||
| Onboarding | below [`ConnectOnboardingView`](../../Shared/Views/NewChat/OnboardingCards.swift#L135) when `shouldShowOnboarding` | `.padding(.horizontal, 20)` (the onboarding cards' margin), `.padding(.bottom, 8)` |
|
||||
|
||||
In the onboarding branch the `.scaleEffect` and `ThemedBackground` are applied to the enclosing `VStack` rather than to each child, so the banner stays below the pages in both toolbar modes.
|
||||
|
||||
### Dismissal
|
||||
|
||||
| Default | Set by | Effect |
|
||||
|---------|--------|--------|
|
||||
| `DEFAULT_GET_STAKE_BANNER_TAPPED` | [`openGetStake()`](../../Shared/Views/ChatList/ChatListView.swift#L408) | the dismiss X appears from then on, while there are chats |
|
||||
| `DEFAULT_GET_STAKE_BANNER_DISMISSED` | the dismiss X | hides the banner in both placements |
|
||||
|
||||
Both are in `hintDefaults`, so "Reset all hints" in the developer settings restores the banner. The X is never offered in the onboarding branch, so the banner cannot be dismissed before the user has a chat.
|
||||
|
||||
Tapping the card opens [`GetStakeView`](../../Shared/Views/Onboarding/WhatsNewView.swift#L834) as an `appSheet`.
|
||||
|
||||
### Shared card chrome
|
||||
|
||||
`BannerCard` (a `ViewModifier`: paddings, minimum height scaled by Dynamic Type, gradient background, rounded corners) and `BannerDismissButton` are declared separately from `GetStakeBanner` so other banners can adopt the same chrome. The gradient reuses `OnboardingCardView.gradientPoints`, `lightStops` and `darkStops`.
|
||||
|
||||
---
|
||||
|
||||
## Source Files
|
||||
|
||||
| File | Path | Key struct | Line |
|
||||
|------|------|------------|------|
|
||||
| Chat list view | [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) | `ChatListView` | [138](../../Shared/Views/ChatList/ChatListView.swift#L142) |
|
||||
| Chat list view | [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) | `ChatListView` | [154](../../Shared/Views/ChatList/ChatListView.swift#L154) |
|
||||
| Chat preview row | [`ChatPreviewView.swift`](../../Shared/Views/ChatList/ChatPreviewView.swift) | `ChatPreviewView` | [12](../../Shared/Views/ChatList/ChatPreviewView.swift#L13) |
|
||||
| Navigation link wrapper | [`ChatListNavLink.swift`](../../Shared/Views/ChatList/ChatListNavLink.swift) | `ChatListNavLink` | [43](../../Shared/Views/ChatList/ChatListNavLink.swift#L44) |
|
||||
| Tag filter tabs | [`TagListView.swift`](../../Shared/Views/ChatList/TagListView.swift) | `TagListView` | [19](../../Shared/Views/ChatList/TagListView.swift#L20) |
|
||||
@@ -294,3 +327,4 @@ The FAB (floating action button) in the bottom-right corner opens the new chat f
|
||||
| Contact connection view | [`ContactConnectionView.swift`](../../Shared/Views/ChatList/ContactConnectionView.swift) | | |
|
||||
| Server summary | [`ServersSummaryView.swift`](../../Shared/Views/ChatList/ServersSummaryView.swift) | | |
|
||||
| One-hand UI card | [`OneHandUICard.swift`](../../Shared/Views/ChatList/OneHandUICard.swift) | | |
|
||||
| Crowdfunding banner | [`GetStakeBanner.swift`](../../Shared/Views/ChatList/GetStakeBanner.swift) | `GetStakeBanner` | [13](../../Shared/Views/ChatList/GetStakeBanner.swift#L13) |
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
| Shared/SimpleXApp.swift | PC1 through PC31 | High | App entry point — initialization affects everything |
|
||||
| Shared/AppDelegate.swift | PC18 | Medium | Push notification registration |
|
||||
| Shared/Views/ChatList/ChatListView.swift | PC1, PC28 | High | Main screen rendering and filtering |
|
||||
| Shared/Views/ChatList/GetStakeBanner.swift | PC1 | Low | Crowdfunding banner and shared banner card chrome |
|
||||
| Shared/Views/Chat/ChatView.swift | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11, PC31 | High | Core conversation UI — most messaging features, channel message rendering |
|
||||
| Shared/Views/Chat/ComposeMessage/ComposeView.swift | PC4, PC6, PC9, PC11, PC31 | High | Message composition — send path for all messages, channel sendAsGroup |
|
||||
| Shared/Views/Chat/ChatItem/ | PC2, PC3, PC5, PC7, PC8, PC9, PC10, PC11 | Medium | Individual message rendering components |
|
||||
|
||||
@@ -240,6 +240,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file)
|
||||
| common/.../common/ui/theme/Theme.kt | spec/services/theme.md | product/views/settings.md |
|
||||
| common/.../common/ui/theme/Color.kt | spec/services/theme.md | product/views/settings.md |
|
||||
| common/.../common/views/chatlist/ChatListView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/GetStakeBanner.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/ChatListNavLinkView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/ChatPreviewView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/UserPicker.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.views.badges.BadgeProduct
|
||||
import chat.simplex.common.views.badges.BadgePurchaseOutcome
|
||||
import chat.simplex.common.views.badges.BadgeStoreError
|
||||
import chat.simplex.common.views.badges.BadgeStoreProductId
|
||||
|
||||
// Play Billing is only in the google flavor, so the Play country stays unknown here
|
||||
fun loadPlayStoreCountry() {}
|
||||
|
||||
const val hasPlatformStore = false
|
||||
|
||||
// No store in this flavor: no product is offered, so the purchase screen shows nothing to buy
|
||||
// TODO [badges] this build pays via Stripe/crypto - the badge service catalog replaces these
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun loadBadgeProducts(oneTimeIds: List<BadgeStoreProductId>, subscriptionIds: List<BadgeStoreProductId>): List<BadgeProduct> {
|
||||
Log.w(TAG, "loadBadgeProducts: no store in the foss build, use the google variant to buy from Play")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun purchaseBadge(id: BadgeStoreProductId, invoiceId: String): BadgePurchaseOutcome =
|
||||
throw BadgeStoreError.StoreUnavailable
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.platform.androidAppContext
|
||||
import chat.simplex.common.platform.androidPlayStoreCountry
|
||||
import chat.simplex.common.platform.mainActivity
|
||||
import chat.simplex.common.views.badges.*
|
||||
import com.android.billingclient.api.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
const val hasPlatformStore = true
|
||||
|
||||
// Requests the country of the Google Play account into [androidPlayStoreCountry].
|
||||
// It stays null when Play is unavailable or the user is not signed in.
|
||||
@@ -29,3 +37,187 @@ fun loadPlayStoreCountry() {
|
||||
override fun onBillingServiceDisconnected() = client.endConnection()
|
||||
})
|
||||
}
|
||||
|
||||
// One long-lived client for badges: ProductDetails obtained from it are passed back to it when the
|
||||
// purchase is launched, and the purchase result arrives on its listener rather than as a return value.
|
||||
// volatile: the listener is called on the main thread, the purchase runs on a background dispatcher
|
||||
@Volatile private var badgeBillingClient: BillingClient? = null
|
||||
@Volatile private var badgeOffers: Map<BadgeStoreProductId, BadgeOffer> = emptyMap()
|
||||
@Volatile private var badgePurchase: CompletableDeferred<BadgePurchaseOutcome>? = null
|
||||
|
||||
// offerToken is null for one-time products, which have no base plan to choose
|
||||
private class BadgeOffer(
|
||||
val id: BadgeStoreProductId,
|
||||
val product: BadgeProduct,
|
||||
val details: ProductDetails,
|
||||
val offerToken: String?
|
||||
)
|
||||
|
||||
suspend fun loadBadgeProducts(oneTimeIds: List<BadgeStoreProductId>, subscriptionIds: List<BadgeStoreProductId>): List<BadgeProduct> {
|
||||
val client = connectedBadgeBillingClient()
|
||||
val details = queryBadgeProducts(client, oneTimeIds, BillingClient.ProductType.INAPP) +
|
||||
queryBadgeProducts(client, subscriptionIds, BillingClient.ProductType.SUBS)
|
||||
val detailsByProductId = details.associateBy { it.productId }
|
||||
val ids = oneTimeIds + subscriptionIds
|
||||
val offers = ids.mapNotNull { id ->
|
||||
val productDetails = detailsByProductId[id.productId]
|
||||
if (productDetails == null) {
|
||||
Log.w(TAG, "loadBadgeProducts: Play returned no product ${id.productId}")
|
||||
null
|
||||
} else {
|
||||
productDetails.badgeOffer(id)
|
||||
}
|
||||
}
|
||||
if (offers.size < ids.size) {
|
||||
// a debug build's applicationIdSuffix is a common cause of Play not knowing the package
|
||||
Log.w(TAG, "loadBadgeProducts: ${offers.size} of ${ids.size} resolved - package ${androidAppContext.packageName}, country ${androidPlayStoreCountry.value ?: "none"}")
|
||||
}
|
||||
badgeOffers = offers.associateBy { it.id }
|
||||
return offers.map { it.product }
|
||||
}
|
||||
|
||||
suspend fun purchaseBadge(id: BadgeStoreProductId, invoiceId: String): BadgePurchaseOutcome {
|
||||
val activity = mainActivity.get() ?: throw BadgeStoreError.StoreUnavailable
|
||||
val client = connectedBadgeBillingClient()
|
||||
val offer = badgeOffers[id] ?: throw BadgeStoreError.ProductUnavailable(id.productId)
|
||||
val details = offer.details
|
||||
val productParams = BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(details)
|
||||
offer.offerToken?.let { productParams.setOfferToken(it) }
|
||||
val params = BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(listOf(productParams.build()))
|
||||
.setObfuscatedAccountId(invoiceId)
|
||||
.build()
|
||||
val purchase = CompletableDeferred<BadgePurchaseOutcome>()
|
||||
badgePurchase = purchase
|
||||
try {
|
||||
val launched = withContext(Dispatchers.Main) { client.launchBillingFlow(activity, params) }
|
||||
if (launched.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
throw BadgeStoreError.BillingError(launched.responseCode, launched.debugMessage)
|
||||
}
|
||||
val outcome = purchase.await()
|
||||
if (outcome is BadgePurchaseOutcome.Purchased) finishBadgePurchase(client, details, outcome.receipt)
|
||||
return outcome
|
||||
} finally {
|
||||
badgePurchase = null
|
||||
}
|
||||
}
|
||||
|
||||
private val badgePurchasesUpdatedListener = PurchasesUpdatedListener { result, purchases ->
|
||||
val pending = badgePurchase ?: return@PurchasesUpdatedListener
|
||||
when {
|
||||
result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null ->
|
||||
pending.complete(badgePurchaseOutcome(purchases))
|
||||
result.responseCode == BillingClient.BillingResponseCode.USER_CANCELED ->
|
||||
pending.complete(BadgePurchaseOutcome.Cancelled)
|
||||
else ->
|
||||
pending.completeExceptionally(BadgeStoreError.BillingError(result.responseCode, result.debugMessage))
|
||||
}
|
||||
}
|
||||
|
||||
private fun badgePurchaseOutcome(purchases: List<Purchase>): BadgePurchaseOutcome {
|
||||
val purchase = purchases.firstOrNull() ?: return BadgePurchaseOutcome.Cancelled
|
||||
if (purchase.purchaseState == Purchase.PurchaseState.PENDING) return BadgePurchaseOutcome.Pending
|
||||
return BadgePurchaseOutcome.Purchased(
|
||||
BadgeStoreReceipt(
|
||||
token = purchase.purchaseToken,
|
||||
productId = purchase.products.firstOrNull() ?: "",
|
||||
orderId = purchase.orderId,
|
||||
invoiceId = purchase.accountIdentifiers?.obfuscatedAccountId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun connectedBadgeBillingClient(): BillingClient {
|
||||
badgeBillingClient?.let { if (it.isReady) return it }
|
||||
val client = BillingClient.newBuilder(androidAppContext)
|
||||
.setListener(badgePurchasesUpdatedListener)
|
||||
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
||||
.build()
|
||||
val connected = CompletableDeferred<BillingResult>()
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
connected.complete(result)
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
badgeBillingClient = null
|
||||
connected.complete(
|
||||
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED).build()
|
||||
)
|
||||
}
|
||||
})
|
||||
val result = connected.await()
|
||||
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
client.endConnection()
|
||||
throw BadgeStoreError.BillingError(result.responseCode, result.debugMessage)
|
||||
}
|
||||
badgeBillingClient = client
|
||||
return client
|
||||
}
|
||||
|
||||
private suspend fun queryBadgeProducts(client: BillingClient, ids: List<BadgeStoreProductId>, productType: String): List<ProductDetails> {
|
||||
// durations of one subscription share a product id, so the same product is queried once
|
||||
val requested = ids.map { it.productId }.distinct()
|
||||
val params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(
|
||||
requested.map {
|
||||
QueryProductDetailsParams.Product.newBuilder().setProductId(it).setProductType(productType).build()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
val queried = CompletableDeferred<List<ProductDetails>>()
|
||||
client.queryProductDetailsAsync(params) { result, productDetailsResult ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
queried.complete(productDetailsResult.productDetailsList)
|
||||
} else {
|
||||
Log.w(TAG, "queryBadgeProducts: $productType requested $requested, failed ${result.responseCode} ${result.debugMessage}")
|
||||
queried.complete(emptyList())
|
||||
}
|
||||
}
|
||||
return queried.await()
|
||||
}
|
||||
|
||||
private fun ProductDetails.badgeOffer(id: BadgeStoreProductId): BadgeOffer? {
|
||||
if (id.basePlanId == null) {
|
||||
val purchase = oneTimePurchaseOfferDetails
|
||||
if (purchase == null) {
|
||||
Log.w(TAG, "badgeOffer: ${id.productId} has no one-time purchase price, type is $productType")
|
||||
return null
|
||||
}
|
||||
val product = BadgeProduct(id, purchase.formattedPrice, purchase.priceAmountMicros, purchase.priceCurrencyCode)
|
||||
return BadgeOffer(id, product, this, offerToken = null)
|
||||
}
|
||||
val offer = subscriptionOfferDetails?.firstOrNull { it.basePlanId == id.basePlanId }
|
||||
if (offer == null) {
|
||||
Log.w(TAG, "badgeOffer: ${id.productId} has no base plan ${id.basePlanId}, Play has ${subscriptionOfferDetails?.map { it.basePlanId }}")
|
||||
return null
|
||||
}
|
||||
val phase = offer.pricingPhases.pricingPhaseList.firstOrNull {
|
||||
it.recurrenceMode == ProductDetails.RecurrenceMode.INFINITE_RECURRING
|
||||
}
|
||||
if (phase == null) {
|
||||
Log.w(TAG, "badgeOffer: ${id.productId} base plan ${id.basePlanId} has no recurring price")
|
||||
return null
|
||||
}
|
||||
val product = BadgeProduct(id, phase.formattedPrice, phase.priceAmountMicros, phase.priceCurrencyCode)
|
||||
return BadgeOffer(id, product, this, offer.offerToken)
|
||||
}
|
||||
|
||||
// nothing is delivered in this build, so the purchase is finished right away; once the service issues
|
||||
// credentials it must only be finished after the credential is stored. One-time products are consumed
|
||||
// so they can be bought again, subscriptions are acknowledged - Play refunds an unacknowledged
|
||||
// purchase after 3 days.
|
||||
private suspend fun finishBadgePurchase(client: BillingClient, details: ProductDetails, receipt: BadgeStoreReceipt) {
|
||||
val done = CompletableDeferred<BillingResult>()
|
||||
if (details.productType == BillingClient.ProductType.SUBS) {
|
||||
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(receipt.token).build()
|
||||
client.acknowledgePurchase(params) { done.complete(it) }
|
||||
} else {
|
||||
val params = ConsumeParams.newBuilder().setPurchaseToken(receipt.token).build()
|
||||
client.consumeAsync(params) { result, _ -> done.complete(result) }
|
||||
}
|
||||
val result = done.await()
|
||||
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
Log.e(TAG, "finishBadgePurchase: ${result.responseCode} ${result.debugMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.badges.BadgeProduct
|
||||
import chat.simplex.common.views.badges.BadgePurchaseOutcome
|
||||
import chat.simplex.common.views.badges.BadgeStoreProductId
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.database.deleteOldChatArchive
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -343,6 +346,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
override fun androidLoadPlayStoreCountry() = loadPlayStoreCountry()
|
||||
|
||||
override val androidHasPlatformStore: Boolean get() = hasPlatformStore
|
||||
|
||||
override suspend fun androidLoadBadgeProducts(oneTimeIds: List<BadgeStoreProductId>, subscriptionIds: List<BadgeStoreProductId>): List<BadgeProduct> = loadBadgeProducts(oneTimeIds, subscriptionIds)
|
||||
|
||||
override suspend fun androidPurchaseBadge(id: BadgeStoreProductId, invoiceId: String): BadgePurchaseOutcome = purchaseBadge(id, invoiceId)
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Composable
|
||||
override fun androidLockPortraitOrientation() {
|
||||
|
||||
@@ -67,6 +67,7 @@ extern char *chat_parse_server(const char *str);
|
||||
extern char *chat_parse_uri(const char *str, const int safe);
|
||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
extern char *chat_valid_name(const char *name);
|
||||
extern char *chat_parse_badge_code(const char *code);
|
||||
extern int chat_json_length(const char *str);
|
||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||
@@ -173,6 +174,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatParseBadgeCode(JNIEnv *env, jclass clazz, jstring code) {
|
||||
const char *_code = (*env)->GetStringUTFChars(env, code, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_parse_badge_code(_code));
|
||||
(*env)->ReleaseStringUTFChars(env, code, _code);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT int JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
||||
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
|
||||
|
||||
@@ -40,6 +40,7 @@ extern char *chat_parse_server(const char *str);
|
||||
extern char *chat_parse_uri(const char *str, const int safe);
|
||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
extern char *chat_valid_name(const char *name);
|
||||
extern char *chat_parse_badge_code(const char *code);
|
||||
extern int chat_json_length(const char *str);
|
||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||
@@ -183,6 +184,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatParseBadgeCode(JNIEnv *env, jclass clazz, jstring code) {
|
||||
const char *_code = encode_to_utf8_chars(env, code);
|
||||
jstring res = decode_to_utf8_string(env, chat_parse_badge_code(_code));
|
||||
(*env)->ReleaseStringUTFChars(env, code, _code);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT int JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
||||
const char *_str = encode_to_utf8_chars(env, str);
|
||||
|
||||
@@ -397,9 +397,11 @@ fun CenterPartOfScreen() {
|
||||
}
|
||||
when (currentChatId.value) {
|
||||
null -> {
|
||||
if (shouldShowOnboarding()) {
|
||||
if (rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
ModalManager.center.showInView()
|
||||
} else if (shouldShowOnboarding()) {
|
||||
ConnectOnboardingView()
|
||||
} else if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -408,8 +410,6 @@ fun CenterPartOfScreen() {
|
||||
) {
|
||||
Text(stringResource(if (chatModel.desktopNoUserNoRemote) MR.strings.no_connected_mobile else MR.strings.no_selected_chat))
|
||||
}
|
||||
} else {
|
||||
ModalManager.center.showInView()
|
||||
}
|
||||
}
|
||||
else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {}
|
||||
|
||||
+212
-1
@@ -102,6 +102,34 @@ object ChannelRelaysModel {
|
||||
}
|
||||
}
|
||||
|
||||
// The badge of whichever profile it was last loaded for, kept current by the badgeChanged event so
|
||||
// that a screen already open shows what the renewal worker did with no command behind it.
|
||||
object BadgeModel {
|
||||
val rhId = mutableStateOf<Long?>(null)
|
||||
val userId = mutableStateOf<Long?>(null)
|
||||
val badgeState = mutableStateOf<BadgeState?>(null)
|
||||
val alert = mutableStateOf<BadgeAlert?>(null)
|
||||
|
||||
// alert follows the state: getUserBadgeState derives it on every read, so a badgeChanged is
|
||||
// never staler than the alert it carries - the invariant a new alert kind must keep
|
||||
fun set(rhId: Long?, userId: Long, badgeState: BadgeState?) {
|
||||
this.rhId.value = rhId
|
||||
this.userId.value = userId
|
||||
this.badgeState.value = badgeState
|
||||
alert.value = badgeState?.alert
|
||||
}
|
||||
|
||||
fun setAlert(rhId: Long?, userId: Long, alert: BadgeAlert) {
|
||||
if (isCurrent(rhId, userId)) {
|
||||
this.alert.value = alert
|
||||
badgeState.value = badgeState.value?.copy(alert = alert)
|
||||
}
|
||||
}
|
||||
|
||||
fun isCurrent(rhId: Long?, userId: Long?): Boolean =
|
||||
this.rhId.value == rhId && this.userId.value == userId
|
||||
}
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
* */
|
||||
@@ -2192,6 +2220,181 @@ data class LocalBadge(
|
||||
val status: BadgeStatus
|
||||
)
|
||||
|
||||
// paidThrough is the only date to show the user: BadgeInfo.badgeExpiry is the credential's expiry,
|
||||
// which outlives entitlement so the credential's window can cover a renewal.
|
||||
@Serializable
|
||||
data class BadgeState(
|
||||
val badgePurchaseId: Long,
|
||||
val purchaseKey: String,
|
||||
val badgeType: BadgeType,
|
||||
val shown: Boolean,
|
||||
val monthsLeft: Int,
|
||||
val paidThrough: Instant,
|
||||
val renewsAt: Instant? = null,
|
||||
val willRenew: Boolean,
|
||||
val alert: BadgeAlert? = null
|
||||
) {
|
||||
val paidThroughText: String get() = badgeDateText(paidThrough)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class StatementEntry(
|
||||
val entryId: String,
|
||||
val changeMonths: Int,
|
||||
val balanceMonths: Int,
|
||||
val balanceStartTs: Instant,
|
||||
val balanceAnchorTs: Instant,
|
||||
val balanceBadgeType: BadgeType,
|
||||
val wasPausedSince: Instant? = null,
|
||||
val createdAt: Instant,
|
||||
val entryType: StatementEntryType
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class StatementEntryType {
|
||||
@Serializable @SerialName("credit") data class Credit(val credit: StatementCreditType): StatementEntryType()
|
||||
@Serializable @SerialName("debit") data class Debit(val debit: StatementDebitType): StatementEntryType()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is Credit -> credit.text
|
||||
is Debit -> debit.text
|
||||
}
|
||||
}
|
||||
|
||||
// the service is deployed ahead of clients, so a type this version does not know keeps its tag
|
||||
@Serializable(with = StatementCreditTypeSerializer::class)
|
||||
sealed class StatementCreditType {
|
||||
@Serializable data class Payment(val invoiceId: String? = null): StatementCreditType()
|
||||
object Code: StatementCreditType()
|
||||
@Serializable data class Charge(val chargeId: String): StatementCreditType()
|
||||
object Support: StatementCreditType()
|
||||
@Serializable data class TransferIn(val fromPurchaseKey: String): StatementCreditType()
|
||||
object Opening: StatementCreditType()
|
||||
data class Unknown(val type: String): StatementCreditType()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is Payment -> "payment"
|
||||
is Code -> "code"
|
||||
is Charge -> "charge"
|
||||
is Support -> "support"
|
||||
is TransferIn -> "transferIn"
|
||||
is Opening -> "opening"
|
||||
is Unknown -> type
|
||||
}
|
||||
}
|
||||
|
||||
object StatementCreditTypeSerializer : KSerializer<StatementCreditType> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StatementCreditType")
|
||||
|
||||
override fun deserialize(decoder: Decoder): StatementCreditType {
|
||||
require(decoder is JsonDecoder)
|
||||
val json = decoder.decodeJsonElement().jsonObject
|
||||
return when (val type = json["type"]?.jsonPrimitive?.content ?: "") {
|
||||
"payment" -> decoder.json.decodeFromJsonElement<StatementCreditType.Payment>(json)
|
||||
"code" -> StatementCreditType.Code
|
||||
"charge" -> decoder.json.decodeFromJsonElement<StatementCreditType.Charge>(json)
|
||||
"support" -> StatementCreditType.Support
|
||||
"transferIn" -> decoder.json.decodeFromJsonElement<StatementCreditType.TransferIn>(json)
|
||||
"opening" -> StatementCreditType.Opening
|
||||
else -> StatementCreditType.Unknown(type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: StatementCreditType) {
|
||||
require(encoder is JsonEncoder)
|
||||
encoder.encodeJsonElement(buildJsonObject {
|
||||
put("type", value.text)
|
||||
when (value) {
|
||||
is StatementCreditType.Payment -> value.invoiceId?.let { put("invoiceId", it) }
|
||||
is StatementCreditType.Charge -> put("chargeId", value.chargeId)
|
||||
is StatementCreditType.TransferIn -> put("fromPurchaseKey", value.fromPurchaseKey)
|
||||
is StatementCreditType.Code, is StatementCreditType.Support, is StatementCreditType.Opening, is StatementCreditType.Unknown -> {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = StatementDebitTypeSerializer::class)
|
||||
sealed class StatementDebitType {
|
||||
object Refund: StatementDebitType()
|
||||
@Serializable data class Upgrade(val toPurchaseKey: String): StatementDebitType()
|
||||
@Serializable data class TransferOut(val toPurchaseKey: String): StatementDebitType()
|
||||
object Support: StatementDebitType()
|
||||
object Badge: StatementDebitType()
|
||||
object Lapse: StatementDebitType()
|
||||
data class Unknown(val type: String): StatementDebitType()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is Refund -> "refund"
|
||||
is Upgrade -> "upgrade"
|
||||
is TransferOut -> "transferOut"
|
||||
is Support -> "support"
|
||||
is Badge -> "badge"
|
||||
is Lapse -> "lapse"
|
||||
is Unknown -> type
|
||||
}
|
||||
}
|
||||
|
||||
object StatementDebitTypeSerializer : KSerializer<StatementDebitType> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StatementDebitType")
|
||||
|
||||
override fun deserialize(decoder: Decoder): StatementDebitType {
|
||||
require(decoder is JsonDecoder)
|
||||
val json = decoder.decodeJsonElement().jsonObject
|
||||
return when (val type = json["type"]?.jsonPrimitive?.content ?: "") {
|
||||
"refund" -> StatementDebitType.Refund
|
||||
"upgrade" -> decoder.json.decodeFromJsonElement<StatementDebitType.Upgrade>(json)
|
||||
"transferOut" -> decoder.json.decodeFromJsonElement<StatementDebitType.TransferOut>(json)
|
||||
"support" -> StatementDebitType.Support
|
||||
"badge" -> StatementDebitType.Badge
|
||||
"lapse" -> StatementDebitType.Lapse
|
||||
else -> StatementDebitType.Unknown(type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: StatementDebitType) {
|
||||
require(encoder is JsonEncoder)
|
||||
encoder.encodeJsonElement(buildJsonObject {
|
||||
put("type", value.text)
|
||||
when (value) {
|
||||
is StatementDebitType.Upgrade -> put("toPurchaseKey", value.toPurchaseKey)
|
||||
is StatementDebitType.TransferOut -> put("toPurchaseKey", value.toPurchaseKey)
|
||||
is StatementDebitType.Refund, is StatementDebitType.Support, is StatementDebitType.Badge, is StatementDebitType.Lapse, is StatementDebitType.Unknown -> {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BadgeAlert(
|
||||
val kind: BadgeAlertKind,
|
||||
val episode: String,
|
||||
val date: Instant,
|
||||
val price: BadgeAlertPrice? = null
|
||||
) {
|
||||
val dateText: String get() = badgeDateText(date)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BadgeAlertPrice(val amount: Long, val currency: String)
|
||||
|
||||
@Serializable
|
||||
enum class BadgeAlertKind {
|
||||
@SerialName("renewalApproaching") RenewalApproaching,
|
||||
@SerialName("paymentIssue") PaymentIssue,
|
||||
@SerialName("subscriptionEnded") SubscriptionEnded,
|
||||
@SerialName("prepaidEnding") PrepaidEnding,
|
||||
@SerialName("supportEnded") SupportEnded
|
||||
}
|
||||
|
||||
private fun badgeDateText(date: Instant): String {
|
||||
val ts = date.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
return ts.toJavaLocalDateTime().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))
|
||||
}
|
||||
|
||||
// the wire proof carried on a profile - opaque to the UI, only round-tripped back to the core (apiPrepareContact)
|
||||
@Serializable
|
||||
data class BadgeProof(
|
||||
@@ -4245,6 +4448,13 @@ enum class MREmojiChar(val value: String) {
|
||||
@SerialName("✅") Check("✅");
|
||||
}
|
||||
|
||||
// set by the core when the file is above the size the sender's badge allows; badgeStatus is null when no proof was sent
|
||||
@Serializable
|
||||
data class FileProhibited(
|
||||
val maxSize: Long,
|
||||
val badgeStatus: BadgeStatus? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CIFile(
|
||||
val fileId: Long,
|
||||
@@ -4253,7 +4463,8 @@ data class CIFile(
|
||||
val fileSource: CryptoFile? = null,
|
||||
val fileStatus: CIFileStatus,
|
||||
val fileProtocol: FileProtocol,
|
||||
val fileExpires: Instant? = null
|
||||
val fileExpires: Instant? = null,
|
||||
val fileProhibited: FileProhibited? = null
|
||||
) {
|
||||
val expired: Boolean = fileExpires != null && fileExpires < Clock.System.now()
|
||||
|
||||
|
||||
+274
-3
@@ -192,6 +192,9 @@ class AppPreferences {
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false)
|
||||
val supporterBannerShown = mkBoolPreference(SHARED_PREFS_SUPPORTER_BANNER_SHOWN, false)
|
||||
val getStakeBannerTapped = mkBoolPreference(SHARED_PREFS_GET_STAKE_BANNER_TAPPED, false)
|
||||
val getStakeBannerDismissed = mkBoolPreference(SHARED_PREFS_GET_STAKE_BANNER_DISMISSED, false)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true)
|
||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||
@@ -273,6 +276,9 @@ class AppPreferences {
|
||||
hintPref(laNoticeShown, false),
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(supporterBannerShown, false),
|
||||
hintPref(getStakeBannerTapped, false),
|
||||
hintPref(getStakeBannerDismissed, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
@@ -464,6 +470,9 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
|
||||
private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown"
|
||||
private const val SHARED_PREFS_SUPPORTER_BANNER_SHOWN = "SupporterBannerShown"
|
||||
private const val SHARED_PREFS_GET_STAKE_BANNER_TAPPED = "GetStakeBannerTapped"
|
||||
private const val SHARED_PREFS_GET_STAKE_BANNER_DISMISSED = "GetStakeBannerDismissed"
|
||||
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
|
||||
private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert"
|
||||
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
|
||||
@@ -559,6 +568,89 @@ object ChatController {
|
||||
|
||||
suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats())
|
||||
|
||||
// log = false because the code is a bearer secret until it is redeemed - it is in the command.
|
||||
// null when the user cancels the retry alert.
|
||||
suspend fun apiRedeemBadgeCode(rh: Long?, userId: Long, code: String): BadgeRedeemResult? {
|
||||
val r = sendCmdWithRetry(rh, CC.ApiRedeemBadgeCode(userId, code), log = false) ?: return null
|
||||
return when {
|
||||
// redeemedBadge is dropped: the user's profile carries what is shown
|
||||
r is API.Result && r.res is CR.BadgeRedeemed -> BadgeRedeemResult.Redeemed(r.res.user.updateRemoteHostId(rh), r.res.newBadge, r.res.badgeState)
|
||||
r is API.Error -> BadgeRedeemResult.Failed(r.err)
|
||||
else -> {
|
||||
// the response type alone - it names a case or a JSON key, never the service's message
|
||||
Log.e(TAG, "apiRedeemBadgeCode: unexpected ${r.responseType}")
|
||||
BadgeRedeemResult.Failed(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localized where the user can act on it; otherwise the error itself, so a screenshot says what happened
|
||||
fun redeemErrorText(err: ChatError?): String {
|
||||
if (err is ChatError.ChatErrorChat && err.errorType is ChatErrorType.CEBadgeRedeemError) {
|
||||
when (val e = err.errorType.badgeRedeemError) {
|
||||
is BadgeRedeemError.InvalidCode -> return generalGetString(MR.strings.badges_error_invalid_code)
|
||||
is BadgeRedeemError.ServiceNotConfigured -> return generalGetString(MR.strings.badges_error_service_not_configured)
|
||||
is BadgeRedeemError.BadgeActive -> return generalGetString(MR.strings.badges_error_already_active)
|
||||
is BadgeRedeemError.ServiceError -> when (e.serviceError) {
|
||||
is BadgeServiceErrorCode.CodeInvalid -> return generalGetString(MR.strings.badges_error_code_invalid)
|
||||
is BadgeServiceErrorCode.CodeUsed -> return generalGetString(MR.strings.badges_error_code_used)
|
||||
is BadgeServiceErrorCode.CodeExpired -> return generalGetString(MR.strings.badges_error_code_expired)
|
||||
is BadgeServiceErrorCode.RateLimited -> return generalGetString(MR.strings.badges_error_rate_limited)
|
||||
is BadgeServiceErrorCode.UnsupportedVersion -> return generalGetString(MR.strings.badges_error_unsupported_version)
|
||||
else -> {}
|
||||
}
|
||||
is BadgeRedeemError.InvalidResponse -> return String.format(generalGetString(MR.strings.badges_error_bad_service_response), e.message)
|
||||
is BadgeRedeemError.UnknownKeyIndex, is BadgeRedeemError.CredentialNotVerified -> return generalGetString(MR.strings.badges_error_credential_not_verified)
|
||||
}
|
||||
}
|
||||
return "${generalGetString(MR.strings.error_prefix)}: ${err?.string ?: generalGetString(MR.strings.badges_error_unknown)}"
|
||||
}
|
||||
|
||||
suspend fun apiGetBadgeState(rh: Long?, userId: Long): BadgeState? {
|
||||
val r = sendCmd(rh, CC.ApiGetBadgeState(userId))
|
||||
if (r is API.Result && r.res is CR.BadgeStateR) return r.res.badgeState
|
||||
throw Exception("apiGetBadgeState: unexpected ${r.responseType}")
|
||||
}
|
||||
|
||||
suspend fun apiGetBadgeLedger(rh: Long?, userId: Long, badgePurchaseId: Long): List<StatementEntry> {
|
||||
val r = sendCmd(rh, CC.ApiGetBadgeLedger(userId, badgePurchaseId))
|
||||
if (r is API.Result && r.res is CR.BadgeLedger) return r.res.badgeLedger
|
||||
throw Exception("apiGetBadgeLedger: unexpected ${r.responseType}")
|
||||
}
|
||||
|
||||
suspend fun apiAckBadgeAlert(rh: Long?, userId: Long, badgePurchaseId: Long, alertKind: BadgeAlertKind, snooze: Boolean, episode: String): BadgeState? {
|
||||
val r = sendCmd(rh, CC.ApiAckBadgeAlert(userId, badgePurchaseId, alertKind, snooze, episode))
|
||||
if (r is API.Result && r.res is CR.BadgeStateR) return r.res.badgeState
|
||||
throw Exception("apiAckBadgeAlert: unexpected ${r.responseType}")
|
||||
}
|
||||
|
||||
// An API call and not a stored flag: the ack is kept on the purchase in core, which then stops
|
||||
// raising this occurrence on every pass and across restarts, or until a snooze lapses.
|
||||
suspend fun ackBadgeAlert(snooze: Boolean) {
|
||||
val rhId = BadgeModel.rhId.value
|
||||
val userId = BadgeModel.userId.value ?: return
|
||||
val purchaseId = BadgeModel.badgeState.value?.badgePurchaseId ?: return
|
||||
val alert = BadgeModel.alert.value ?: return
|
||||
try {
|
||||
val badgeState = apiAckBadgeAlert(rhId, userId, purchaseId, alert.kind, snooze = snooze, episode = alert.episode)
|
||||
withContext(Dispatchers.Main) { BadgeModel.set(rhId, userId, badgeState) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "ackBadgeAlert: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Not thrown: a failed badge read must not stop the app starting, and the model is left alone
|
||||
// rather than set to nil, which would read as "no badge".
|
||||
private suspend fun loadBadgeState(rhId: Long?) {
|
||||
try {
|
||||
val userId = currentUserId("loadBadgeState")
|
||||
val badgeState = apiGetBadgeState(rhId, userId)
|
||||
withContext(Dispatchers.Main) { BadgeModel.set(rhId, userId, badgeState) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "loadBadgeState: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock {
|
||||
val userId = chatModel.currentUser.value?.userId
|
||||
if (userId == null) {
|
||||
@@ -680,6 +772,7 @@ object ChatController {
|
||||
chatModel.userTags.value = if (hasUser) apiGetChatTags(rhId) ?: emptyList() else emptyList()
|
||||
chatModel.activeChatTagFilter.value = null
|
||||
chatModel.updateChatTags(rhId)
|
||||
if (hasUser) loadBadgeState(rhId)
|
||||
}
|
||||
|
||||
// Spec: spec/api.md#startReceiver
|
||||
@@ -743,8 +836,8 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, inProgress: MutableState<Boolean>? = null, retryNum: Int = 0): API? {
|
||||
val r = sendCmd(rhId, cmd, retryNum = retryNum)
|
||||
private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, inProgress: MutableState<Boolean>? = null, retryNum: Int = 0, log: Boolean = true): API? {
|
||||
val r = sendCmd(rhId, cmd, retryNum = retryNum, log = log)
|
||||
val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null
|
||||
if ((inProgress == null || inProgress.value) && alert != null) {
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
@@ -764,7 +857,7 @@ object ChatController {
|
||||
safeResume(
|
||||
runCatching {
|
||||
coroutineScope {
|
||||
sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1)
|
||||
sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1, log = log)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -827,6 +920,10 @@ object ChatController {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
is AgentErrorType.AGENT ->
|
||||
if (e.agentErr is SMPAgentError.A_SERVICE && e.agentErr.serviceError is AgentServiceError.Timeout) {
|
||||
return MR.strings.connection_timeout to generalGetString(MR.strings.service_request_timeout_desc)
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
return null
|
||||
@@ -3472,6 +3569,22 @@ object ChatController {
|
||||
chatModel.chatsContext.updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.BadgeChanged ->
|
||||
if (rhId == chatModel.remoteHostId()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// read by core after retiring or presenting, so it carries the profile badge as changed
|
||||
chatModel.updateUser(r.user.updateRemoteHostId(rhId))
|
||||
if (active(r.user)) {
|
||||
BadgeModel.set(rhId, r.user.userId, r.badgeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.BadgeAlertR ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
BadgeModel.setAlert(rhId, r.user.userId, r.badgeAlert)
|
||||
}
|
||||
}
|
||||
else ->
|
||||
Log.d(TAG , "unsupported event: ${msg.responseType}")
|
||||
}
|
||||
@@ -3769,6 +3882,12 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class BadgeRedeemResult {
|
||||
class Redeemed(val user: User, val newBadge: Boolean, val badgeState: BadgeState?): BadgeRedeemResult()
|
||||
// err is null for a response of an unexpected type, which is logged where it is received
|
||||
class Failed(val err: ChatError?): BadgeRedeemResult()
|
||||
}
|
||||
|
||||
// ChatCommand
|
||||
// Spec: spec/api.md#CC
|
||||
sealed class CC {
|
||||
@@ -3947,6 +4066,11 @@ sealed class CC {
|
||||
class ApiUploadStandaloneFile(val userId: Long, val file: CryptoFile): CC()
|
||||
class ApiDownloadStandaloneFile(val userId: Long, val url: String, val file: CryptoFile): CC()
|
||||
class ApiStandaloneFileInfo(val url: String): CC()
|
||||
// badges
|
||||
class ApiRedeemBadgeCode(val userId: Long, val code: String): CC()
|
||||
class ApiGetBadgeState(val userId: Long): CC()
|
||||
class ApiGetBadgeLedger(val userId: Long, val badgePurchaseId: Long): CC()
|
||||
class ApiAckBadgeAlert(val userId: Long, val badgePurchaseId: Long, val alertKind: BadgeAlertKind, val snooze: Boolean, val episode: String): CC()
|
||||
// misc
|
||||
class ShowVersion(): CC()
|
||||
class ResetAgentServersStats(): CC()
|
||||
@@ -4168,6 +4292,10 @@ sealed class CC {
|
||||
is ApiUploadStandaloneFile -> "/_upload $userId ${file.filePath}"
|
||||
is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}"
|
||||
is ApiStandaloneFileInfo -> "/_download info $url"
|
||||
is ApiRedeemBadgeCode -> "/_redeem_badge_code $userId $code"
|
||||
is ApiGetBadgeState -> "/_badge state $userId"
|
||||
is ApiGetBadgeLedger -> "/_badge ledger $userId $badgePurchaseId"
|
||||
is ApiAckBadgeAlert -> "/_badge ack $userId $badgePurchaseId ${badgeAlertKindParam(alertKind)} ${onOff(snooze)} $episode"
|
||||
is ShowVersion -> "/version"
|
||||
is ResetAgentServersStats -> "/reset servers stats"
|
||||
is GetAgentSubsTotal -> "/get subs total $userId"
|
||||
@@ -4348,6 +4476,10 @@ sealed class CC {
|
||||
is ApiUploadStandaloneFile -> "apiUploadStandaloneFile"
|
||||
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
|
||||
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
|
||||
is ApiRedeemBadgeCode -> "apiRedeemBadgeCode"
|
||||
is ApiGetBadgeState -> "apiGetBadgeState"
|
||||
is ApiGetBadgeLedger -> "apiGetBadgeLedger"
|
||||
is ApiAckBadgeAlert -> "apiAckBadgeAlert"
|
||||
is ShowVersion -> "showVersion"
|
||||
is ResetAgentServersStats -> "resetAgentServersStats"
|
||||
is GetAgentSubsTotal -> "getAgentSubsTotal"
|
||||
@@ -4369,6 +4501,8 @@ sealed class CC {
|
||||
is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd))
|
||||
is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd))
|
||||
is TestStorageEncryption -> TestStorageEncryption(obfuscate(key))
|
||||
// a code is a bearer secret until it is redeemed, and the terminal shows and copies cmdString
|
||||
is ApiRedeemBadgeCode -> ApiRedeemBadgeCode(userId, obfuscate(code))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@@ -4404,6 +4538,15 @@ sealed class CC {
|
||||
|
||||
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
// /_badge ack takes the kind in core's text encoding, not the JSON tag
|
||||
private fun badgeAlertKindParam(kind: BadgeAlertKind): String = when (kind) {
|
||||
BadgeAlertKind.RenewalApproaching -> "renewal_approaching"
|
||||
BadgeAlertKind.PaymentIssue -> "payment_issue"
|
||||
BadgeAlertKind.SubscriptionEnded -> "subscription_ended"
|
||||
BadgeAlertKind.PrepaidEnding -> "prepaid_ending"
|
||||
BadgeAlertKind.SupportEnded -> "support_ended"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
@@ -6722,6 +6865,13 @@ sealed class CR {
|
||||
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||
@Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR()
|
||||
@Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR()
|
||||
// badges
|
||||
// the full user, not UserRef: its profile carries the badge that setUserBadge just stored
|
||||
@Serializable @SerialName("badgeRedeemed") class BadgeRedeemed(val user: User, val redeemedBadge: LocalBadge, val newBadge: Boolean, val badgeState: BadgeState?): CR()
|
||||
@Serializable @SerialName("badgeState") class BadgeStateR(val user: UserRef, val badgeState: BadgeState?): CR()
|
||||
@Serializable @SerialName("badgeLedger") class BadgeLedger(val user: UserRef, val badgeLedger: List<StatementEntry>): CR()
|
||||
@Serializable @SerialName("badgeChanged") class BadgeChanged(val user: User, val badgeState: BadgeState?): CR()
|
||||
@Serializable @SerialName("badgeAlert") class BadgeAlertR(val user: UserRef, val badgeAlert: BadgeAlert): CR()
|
||||
// general
|
||||
@Serializable class Response(val type: String, val json: String): CR()
|
||||
@Serializable class Invalid(val str: String): CR()
|
||||
@@ -6908,6 +7058,11 @@ sealed class CR {
|
||||
is ArchiveExported -> "archiveExported"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is AppSettingsR -> "appSettings"
|
||||
is BadgeRedeemed -> "badgeRedeemed"
|
||||
is BadgeStateR -> "badgeState"
|
||||
is BadgeLedger -> "badgeLedger"
|
||||
is BadgeChanged -> "badgeChanged"
|
||||
is BadgeAlertR -> "badgeAlert"
|
||||
is Response -> "* $type"
|
||||
is Invalid -> "* invalid json"
|
||||
}
|
||||
@@ -7111,6 +7266,11 @@ sealed class CR {
|
||||
is ArchiveExported -> "${archiveErrors.map { it.string } }"
|
||||
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
||||
is AppSettingsR -> json.encodeToString(appSettings)
|
||||
is BadgeRedeemed -> withUser(user, "redeemedBadge: ${json.encodeToString(redeemedBadge)}\nnewBadge: $newBadge\nbadgeState: ${json.encodeToString(badgeState)}")
|
||||
is BadgeStateR -> withUser(user, json.encodeToString(badgeState))
|
||||
is BadgeLedger -> withUser(user, json.encodeToString(badgeLedger))
|
||||
is BadgeChanged -> withUser(user, json.encodeToString(badgeState))
|
||||
is BadgeAlertR -> withUser(user, json.encodeToString(badgeAlert))
|
||||
is Response -> json
|
||||
is Invalid -> str
|
||||
}
|
||||
@@ -7162,6 +7322,97 @@ sealed class SimplexDomainError {
|
||||
@Serializable @SerialName("unknownDomain") object UnknownDomain : SimplexDomainError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class BadgeRedeemError {
|
||||
val string: String get() = when (this) {
|
||||
is InvalidCode -> "invalidCode"
|
||||
is ServiceNotConfigured -> "serviceNotConfigured"
|
||||
is BadgeActive -> "badgeActive"
|
||||
is ServiceError -> "serviceError ${serviceError.text}"
|
||||
is InvalidResponse -> "invalidResponse $message"
|
||||
is UnknownKeyIndex -> "unknownKeyIndex"
|
||||
is CredentialNotVerified -> "credentialNotVerified"
|
||||
}
|
||||
@Serializable @SerialName("invalidCode") object InvalidCode : BadgeRedeemError()
|
||||
@Serializable @SerialName("serviceNotConfigured") object ServiceNotConfigured : BadgeRedeemError()
|
||||
@Serializable @SerialName("badgeActive") object BadgeActive : BadgeRedeemError()
|
||||
@Serializable @SerialName("serviceError") class ServiceError(val serviceError: BadgeServiceErrorCode) : BadgeRedeemError()
|
||||
@Serializable @SerialName("invalidResponse") class InvalidResponse(val message: String) : BadgeRedeemError()
|
||||
@Serializable @SerialName("unknownKeyIndex") object UnknownKeyIndex : BadgeRedeemError()
|
||||
@Serializable @SerialName("credentialNotVerified") object CredentialNotVerified : BadgeRedeemError()
|
||||
}
|
||||
|
||||
// the service is deployed ahead of clients, so a code this version does not know keeps its tag
|
||||
@Serializable(with = BadgeServiceErrorCodeSerializer::class)
|
||||
sealed class BadgeServiceErrorCode {
|
||||
object BadRequest: BadgeServiceErrorCode()
|
||||
object UnsupportedVersion: BadgeServiceErrorCode()
|
||||
object UnknownPurchaseKey: BadgeServiceErrorCode()
|
||||
object UnknownOfferId: BadgeServiceErrorCode()
|
||||
object OfferDisabled: BadgeServiceErrorCode()
|
||||
object OfferMismatch: BadgeServiceErrorCode()
|
||||
object ProductUnavailable: BadgeServiceErrorCode()
|
||||
object PaymentNotEntitled: BadgeServiceErrorCode()
|
||||
object PaymentPending: BadgeServiceErrorCode()
|
||||
object ProviderUnavailable: BadgeServiceErrorCode()
|
||||
object RateLimited: BadgeServiceErrorCode()
|
||||
object CodeInvalid: BadgeServiceErrorCode()
|
||||
object CodeUsed: BadgeServiceErrorCode()
|
||||
object CodeExpired: BadgeServiceErrorCode()
|
||||
object ReceiptInvalid: BadgeServiceErrorCode()
|
||||
object ReceiptUsed: BadgeServiceErrorCode()
|
||||
object Internal: BadgeServiceErrorCode()
|
||||
data class Unknown(val code: String): BadgeServiceErrorCode()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is BadRequest -> "bad_request"
|
||||
is UnsupportedVersion -> "unsupported_version"
|
||||
is UnknownPurchaseKey -> "unknown_purchase_key"
|
||||
is UnknownOfferId -> "unknown_offer_id"
|
||||
is OfferDisabled -> "offer_disabled"
|
||||
is OfferMismatch -> "offer_mismatch"
|
||||
is ProductUnavailable -> "product_unavailable"
|
||||
is PaymentNotEntitled -> "payment_not_entitled"
|
||||
is PaymentPending -> "payment_pending"
|
||||
is ProviderUnavailable -> "provider_unavailable"
|
||||
is RateLimited -> "rate_limited"
|
||||
is CodeInvalid -> "code_invalid"
|
||||
is CodeUsed -> "code_used"
|
||||
is CodeExpired -> "code_expired"
|
||||
is ReceiptInvalid -> "receipt_invalid"
|
||||
is ReceiptUsed -> "receipt_used"
|
||||
is Internal -> "internal"
|
||||
is Unknown -> code
|
||||
}
|
||||
}
|
||||
|
||||
object BadgeServiceErrorCodeSerializer : KSerializer<BadgeServiceErrorCode> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BadgeServiceErrorCode", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): BadgeServiceErrorCode =
|
||||
when (val v = decoder.decodeString()) {
|
||||
"bad_request" -> BadgeServiceErrorCode.BadRequest
|
||||
"unsupported_version" -> BadgeServiceErrorCode.UnsupportedVersion
|
||||
"unknown_purchase_key" -> BadgeServiceErrorCode.UnknownPurchaseKey
|
||||
"unknown_offer_id" -> BadgeServiceErrorCode.UnknownOfferId
|
||||
"offer_disabled" -> BadgeServiceErrorCode.OfferDisabled
|
||||
"offer_mismatch" -> BadgeServiceErrorCode.OfferMismatch
|
||||
"product_unavailable" -> BadgeServiceErrorCode.ProductUnavailable
|
||||
"payment_not_entitled" -> BadgeServiceErrorCode.PaymentNotEntitled
|
||||
"payment_pending" -> BadgeServiceErrorCode.PaymentPending
|
||||
"provider_unavailable" -> BadgeServiceErrorCode.ProviderUnavailable
|
||||
"rate_limited" -> BadgeServiceErrorCode.RateLimited
|
||||
"code_invalid" -> BadgeServiceErrorCode.CodeInvalid
|
||||
"code_used" -> BadgeServiceErrorCode.CodeUsed
|
||||
"code_expired" -> BadgeServiceErrorCode.CodeExpired
|
||||
"receipt_invalid" -> BadgeServiceErrorCode.ReceiptInvalid
|
||||
"receipt_used" -> BadgeServiceErrorCode.ReceiptUsed
|
||||
"internal" -> BadgeServiceErrorCode.Internal
|
||||
else -> BadgeServiceErrorCode.Unknown(v)
|
||||
}
|
||||
override fun serialize(encoder: Encoder, value: BadgeServiceErrorCode) = encoder.encodeString(value.text)
|
||||
}
|
||||
|
||||
data class ConnectionPlanResult(
|
||||
val connLink: CreatedConnLink,
|
||||
val planSimplexName: SimplexNameInfo?,
|
||||
@@ -7567,6 +7818,7 @@ sealed class ChatErrorType {
|
||||
is AgentVersion -> "agentVersion"
|
||||
is AgentNoSubResult -> "agentNoSubResult"
|
||||
is CommandError -> "commandError $message"
|
||||
is CEBadgeRedeemError -> "badgeRedeemError ${badgeRedeemError.string}"
|
||||
is ServerProtocol -> "serverProtocol"
|
||||
is AgentCommandError -> "agentCommandError"
|
||||
is InvalidFileDescription -> "invalidFileDescription"
|
||||
@@ -7651,6 +7903,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("agentVersion") object AgentVersion: ChatErrorType()
|
||||
@Serializable @SerialName("agentNoSubResult") class AgentNoSubResult(val agentConnId: String): ChatErrorType()
|
||||
@Serializable @SerialName("commandError") class CommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("badgeRedeemError") class CEBadgeRedeemError(val badgeRedeemError: BadgeRedeemError): ChatErrorType()
|
||||
@Serializable @SerialName("serverProtocol") object ServerProtocol: ChatErrorType()
|
||||
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
|
||||
@@ -8085,6 +8338,7 @@ sealed class SMPAgentError {
|
||||
is A_CRYPTO -> "A_CRYPTO"
|
||||
is A_DUPLICATE -> "A_DUPLICATE"
|
||||
is A_QUEUE -> "A_QUEUE"
|
||||
is A_SERVICE -> "A_SERVICE ${serviceError.string}"
|
||||
}
|
||||
@Serializable @SerialName("A_MESSAGE") object A_MESSAGE: SMPAgentError()
|
||||
@Serializable @SerialName("A_PROHIBITED") class A_PROHIBITED(val prohibitedErr: String): SMPAgentError()
|
||||
@@ -8093,6 +8347,23 @@ sealed class SMPAgentError {
|
||||
@Serializable @SerialName("A_CRYPTO") object A_CRYPTO: SMPAgentError()
|
||||
@Serializable @SerialName("A_DUPLICATE") object A_DUPLICATE: SMPAgentError()
|
||||
@Serializable @SerialName("A_QUEUE") class A_QUEUE(val queueErr: String): SMPAgentError()
|
||||
@Serializable @SerialName("A_SERVICE") class A_SERVICE(val serviceError: AgentServiceError): SMPAgentError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class AgentServiceError {
|
||||
val string: String get() = when (this) {
|
||||
is Rejected -> "rejected"
|
||||
is Timeout -> "timeout"
|
||||
is NoPendingRequest -> "noPendingRequest"
|
||||
is NotDRAddress -> "notDRAddress"
|
||||
is BadSignature -> "badSignature"
|
||||
}
|
||||
@Serializable @SerialName("rejected") object Rejected: AgentServiceError()
|
||||
@Serializable @SerialName("timeout") object Timeout: AgentServiceError()
|
||||
@Serializable @SerialName("noPendingRequest") object NoPendingRequest: AgentServiceError()
|
||||
@Serializable @SerialName("notDRAddress") object NotDRAddress: AgentServiceError()
|
||||
@Serializable @SerialName("badSignature") object BadSignature: AgentServiceError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -33,12 +33,20 @@ external fun chatParseServer(str: String): String
|
||||
external fun chatParseUri(str: String, safe: Int): String
|
||||
external fun chatPasswordHash(pwd: String, salt: String): String
|
||||
external fun chatValidName(name: String): String
|
||||
external fun chatParseBadgeCode(code: String): String
|
||||
external fun chatJsonLength(str: String): Int
|
||||
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
||||
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
||||
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
||||
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String
|
||||
|
||||
// the code in core's canonical form (prefix and 20 characters, no separators), or null when core rejects it.
|
||||
// The check character is verified by core via FFI, not re-implemented here, so the two cannot disagree.
|
||||
fun parseBadgeCode(s: String): String? {
|
||||
val canonical = chatParseBadgeCode(s)
|
||||
return if (canonical.isEmpty()) null else canonical
|
||||
}
|
||||
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
|
||||
+13
@@ -9,6 +9,10 @@ import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatId
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.badges.BadgeProduct
|
||||
import chat.simplex.common.views.badges.BadgePurchaseOutcome
|
||||
import chat.simplex.common.views.badges.BadgeStoreError
|
||||
import chat.simplex.common.views.badges.BadgeStoreProductId
|
||||
import kotlinx.coroutines.Job
|
||||
import java.io.Closeable
|
||||
|
||||
@@ -31,6 +35,15 @@ interface PlatformInterface {
|
||||
fun androidIsXiaomiDevice(): Boolean = false
|
||||
// Requests the Google Play account country into [androidPlayStoreCountry]
|
||||
fun androidLoadPlayStoreCountry() {}
|
||||
// Play Billing, only implemented in the google flavor
|
||||
// TODO [badges] desktop and foss pay via Stripe/crypto - these defaults leave them without any
|
||||
// product until that path is implemented
|
||||
val androidHasPlatformStore: Boolean get() = false
|
||||
suspend fun androidLoadBadgeProducts(oneTimeIds: List<BadgeStoreProductId>, subscriptionIds: List<BadgeStoreProductId>): List<BadgeProduct> {
|
||||
Log.w(TAG, "androidLoadBadgeProducts: no store on this platform")
|
||||
return emptyList()
|
||||
}
|
||||
suspend fun androidPurchaseBadge(id: BadgeStoreProductId, invoiceId: String): BadgePurchaseOutcome = throw BadgeStoreError.StoreUnavailable
|
||||
val androidApiLevel: Int? get() = null
|
||||
// The build distributed via Google Play, which has to follow its policies
|
||||
val androidIsPlayStoreBuild: Boolean get() = false
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.common.platform.*
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.UUID
|
||||
|
||||
// a subscription is one store product containing a base plan per duration, so a purchasable badge
|
||||
// is identified by both; one-time products have no base plan
|
||||
data class BadgeStoreProductId(val productId: String, val basePlanId: String? = null)
|
||||
|
||||
// TODO [badges] ids will come from app config and prices from the badge service catalog;
|
||||
// hardcoded here so the Play Store integration can be tested before the purchase API lands.
|
||||
fun badgeStoreProductId(level: BadgeLevel, period: BadgePeriod): BadgeStoreProductId = when (level) {
|
||||
BadgeLevel.Supporter -> when (period) {
|
||||
BadgePeriod.OneMonth -> BadgeStoreProductId("badge_supporter_01")
|
||||
BadgePeriod.Monthly -> BadgeStoreProductId("subscr_badge_supporter_01", "subscr-badge-supporter-month-02")
|
||||
BadgePeriod.Annual -> BadgeStoreProductId("subscr_badge_supporter_01", "subscr-badge-supporter-year-01")
|
||||
}
|
||||
BadgeLevel.Legend -> when (period) {
|
||||
BadgePeriod.OneMonth -> BadgeStoreProductId("badge_legend_01")
|
||||
BadgePeriod.Monthly -> BadgeStoreProductId("subscr_badge_legend_01", "subscr-badge-legend-month-01")
|
||||
BadgePeriod.Annual -> BadgeStoreProductId("subscr_badge_legend_01", "subscr-badge-legend-year-01")
|
||||
}
|
||||
}
|
||||
|
||||
val badgeStoreProductIds: List<BadgeStoreProductId> = BadgeLevel.entries.flatMap { level ->
|
||||
BadgePeriod.entries.map { badgeStoreProductId(level, it) }
|
||||
}
|
||||
|
||||
// TODO [badges] replaced by APIGetBadgeInvoice, which creates the invoice row and returns its id.
|
||||
// Sent to Play as obfuscatedAccountId and echoed back on the purchase, which is how the service
|
||||
// learns which invoice a store transaction settles.
|
||||
fun newBadgeInvoiceId(): String = UUID.randomUUID().toString()
|
||||
|
||||
// what the platform store knows about one product; ProductDetails cannot cross into commonMain
|
||||
data class BadgeProduct(
|
||||
val id: BadgeStoreProductId,
|
||||
val displayPrice: String,
|
||||
val priceMicros: Long,
|
||||
val currencyCode: String
|
||||
)
|
||||
|
||||
sealed class BadgePrice {
|
||||
object Loading: BadgePrice()
|
||||
class Price(val price: String): BadgePrice()
|
||||
object Unavailable: BadgePrice()
|
||||
|
||||
val canPurchase: Boolean
|
||||
get() = when (this) {
|
||||
is Price -> true
|
||||
is Loading, is Unavailable -> false
|
||||
}
|
||||
}
|
||||
|
||||
data class BadgeStoreReceipt(
|
||||
// the token the badge service verifies with the Publisher API
|
||||
val token: String,
|
||||
val productId: String,
|
||||
val orderId: String?,
|
||||
val invoiceId: String?,
|
||||
// only set for test products - a real Play purchase has no environment to report
|
||||
val environment: String? = null
|
||||
)
|
||||
|
||||
// TODO [badges] Play Billing has no offline product configuration. Set to true to price the screens
|
||||
// and walk the purchase flow without Play Console products; the purchase is simulated and its
|
||||
// receipt says so.
|
||||
const val useBadgeTestProducts = false
|
||||
|
||||
private fun testProduct(level: BadgeLevel, period: BadgePeriod, priceMicros: Long) =
|
||||
BadgeProduct(badgeStoreProductId(level, period), "\$${priceMicros / 1_000_000}.00", priceMicros, "USD")
|
||||
|
||||
private val testBadgeProducts: List<BadgeProduct> = listOf(
|
||||
testProduct(BadgeLevel.Supporter, BadgePeriod.OneMonth, 7_000_000),
|
||||
testProduct(BadgeLevel.Supporter, BadgePeriod.Monthly, 7_000_000),
|
||||
testProduct(BadgeLevel.Supporter, BadgePeriod.Annual, 42_000_000),
|
||||
testProduct(BadgeLevel.Legend, BadgePeriod.OneMonth, 70_000_000),
|
||||
testProduct(BadgeLevel.Legend, BadgePeriod.Monthly, 70_000_000),
|
||||
testProduct(BadgeLevel.Legend, BadgePeriod.Annual, 420_000_000)
|
||||
)
|
||||
|
||||
sealed class BadgePurchaseOutcome {
|
||||
class Purchased(val receipt: BadgeStoreReceipt): BadgePurchaseOutcome()
|
||||
object Pending: BadgePurchaseOutcome()
|
||||
object Cancelled: BadgePurchaseOutcome()
|
||||
}
|
||||
|
||||
sealed class BadgeStoreError: Exception() {
|
||||
class ProductUnavailable(val productId: String): BadgeStoreError()
|
||||
class BillingError(val responseCode: Int, val debugMessage: String): BadgeStoreError()
|
||||
object StoreUnavailable: BadgeStoreError()
|
||||
|
||||
override val message: String
|
||||
get() = when (this) {
|
||||
is ProductUnavailable -> "productUnavailable(productId: $productId)"
|
||||
is BillingError -> "billingError(responseCode: $responseCode, $debugMessage)"
|
||||
is StoreUnavailable -> "storeUnavailable"
|
||||
}
|
||||
}
|
||||
|
||||
object BadgeStore {
|
||||
private enum class LoadState { NotLoaded, Loading, Loaded, Failed }
|
||||
|
||||
private val state = mutableStateOf(LoadState.NotLoaded)
|
||||
// snapshot state so a composable reading only the products still recomposes when they arrive
|
||||
private val products = mutableStateOf<Map<BadgeStoreProductId, BadgeProduct>>(emptyMap())
|
||||
|
||||
fun price(level: BadgeLevel, period: BadgePeriod): BadgePrice = when (state.value) {
|
||||
LoadState.NotLoaded, LoadState.Loading -> BadgePrice.Loading
|
||||
LoadState.Loaded, LoadState.Failed -> {
|
||||
val p = products.value[badgeStoreProductId(level, period)]
|
||||
if (p != null) BadgePrice.Price(compactPrice(p)) else BadgePrice.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
fun annualSavings(level: BadgeLevel): Int? {
|
||||
val monthly = products.value[badgeStoreProductId(level, BadgePeriod.Monthly)] ?: return null
|
||||
val annual = products.value[badgeStoreProductId(level, BadgePeriod.Annual)] ?: return null
|
||||
val year = monthly.priceMicros * 12
|
||||
if (year <= 0 || annual.priceMicros >= year) return null
|
||||
val percent = Math.round((year - annual.priceMicros).toDouble() / year * 100).toInt()
|
||||
return if (percent > 0) percent else null
|
||||
}
|
||||
|
||||
suspend fun load() {
|
||||
if (!startLoading()) return
|
||||
try {
|
||||
// TODO [badges] desktop and the foss build will price from the badge service catalog and pay
|
||||
// via Stripe/crypto instead of a store; only the google build reaches the platform store
|
||||
val loaded = if (useBadgeTestProducts) testBadgeProducts else platform.androidLoadBadgeProducts(
|
||||
oneTimeIds = badgeStoreProductIds.filter { it.basePlanId == null },
|
||||
subscriptionIds = badgeStoreProductIds.filter { it.basePlanId != null }
|
||||
)
|
||||
val byId = loaded.associateBy { it.id }
|
||||
val missing = badgeStoreProductIds.filter { !byId.containsKey(it) }
|
||||
if (missing.isNotEmpty()) {
|
||||
Log.w(TAG, "BadgeStore.load: no product returned for ${missing.joinToString(", ")}")
|
||||
}
|
||||
products.value = byId
|
||||
state.value = LoadState.Loaded
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "BadgeStore.load: ${e.stackTraceToString()}")
|
||||
state.value = LoadState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun purchase(level: BadgeLevel, period: BadgePeriod, invoiceId: String): BadgePurchaseOutcome {
|
||||
val id = badgeStoreProductId(level, period)
|
||||
if (!products.value.containsKey(id)) throw BadgeStoreError.ProductUnavailable(id.productId)
|
||||
if (useBadgeTestProducts) {
|
||||
return BadgePurchaseOutcome.Purchased(
|
||||
BadgeStoreReceipt(
|
||||
token = "test-${UUID.randomUUID()}",
|
||||
productId = id.productId,
|
||||
orderId = null,
|
||||
invoiceId = invoiceId,
|
||||
environment = "test products"
|
||||
)
|
||||
)
|
||||
}
|
||||
return platform.androidPurchaseBadge(id, invoiceId)
|
||||
}
|
||||
|
||||
private fun startLoading(): Boolean = when (state.value) {
|
||||
LoadState.NotLoaded, LoadState.Failed -> {
|
||||
state.value = LoadState.Loading
|
||||
true
|
||||
}
|
||||
LoadState.Loading, LoadState.Loaded -> false
|
||||
}
|
||||
}
|
||||
|
||||
// drops the fraction from whole amounts ("$7", not "$7.00") in the product's own currency;
|
||||
// BadgeProduct.displayPrice remains the exact form for views that need the cents
|
||||
private fun compactPrice(product: BadgeProduct): String {
|
||||
if (product.priceMicros % 1_000_000L != 0L) return product.displayPrice
|
||||
return try {
|
||||
val format = NumberFormat.getCurrencyInstance()
|
||||
format.currency = Currency.getInstance(product.currencyCode)
|
||||
format.maximumFractionDigits = 0
|
||||
format.format(product.priceMicros / 1_000_000L)
|
||||
} catch (e: Exception) {
|
||||
product.displayPrice
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeInfo
|
||||
import chat.simplex.common.model.BadgeStatus
|
||||
import chat.simplex.common.model.LocalBadge
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.NameWithBadge
|
||||
import chat.simplex.common.views.helpers.ProfileImage
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@Composable
|
||||
fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier, trailing: @Composable () -> Unit = {}) {
|
||||
val user = chatModel.currentUser.value
|
||||
val displayName = user?.displayName ?: stringResource(MR.strings.badges_preview_my_nickname)
|
||||
val previewBadge = LocalBadge(
|
||||
// fabricated for the preview: the status is given here, and NameBadge renders from it alone
|
||||
badge = BadgeInfo(badgeType = level.badgeType, badgeExpiry = Instant.DISTANT_FUTURE),
|
||||
status = BadgeStatus.Active
|
||||
)
|
||||
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ProfileImage(size = 128.dp, image = user?.image)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
NameWithBadge(
|
||||
name = displayName,
|
||||
badge = previewBadge,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
trailing()
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.views.onboarding.ReadableTextWithLink
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgesHowItWorksView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_how_it_works_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p1), style = MaterialTheme.typography.body1)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p2), style = MaterialTheme.typography.body1)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p3), style = MaterialTheme.typography.body1)
|
||||
ReadableTextWithLink(MR.strings.badges_how_it_works_read_more_with_link, "https://simplex.chat/blog/20260919-simplex-supporter-badges.html")
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import itemHPadding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import chat.simplex.common.model.BadgeState
|
||||
import chat.simplex.common.model.StatementCreditType
|
||||
import chat.simplex.common.model.StatementDebitType
|
||||
import chat.simplex.common.model.StatementEntry
|
||||
import chat.simplex.common.model.StatementEntryType
|
||||
import chat.simplex.common.model.json
|
||||
import chat.simplex.common.model.localDate
|
||||
import chat.simplex.common.model.localTimestamp
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.shareText
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.ModalView
|
||||
import chat.simplex.common.views.helpers.ShareButton
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import chat.simplex.res.MR
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun BadgesLedgerView(badgeState: BadgeState, close: () -> Unit) {
|
||||
val entries = remember { mutableStateOf<List<StatementEntry>?>(null) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val user = chatModel.currentUser.value ?: return@LaunchedEffect
|
||||
withBGApi {
|
||||
try {
|
||||
val ledger = chatModel.controller.apiGetBadgeLedger(chatModel.remoteHostId(), user.userId, badgeState.badgePurchaseId)
|
||||
withContext(Dispatchers.Main) { entries.value = ledger }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "apiGetBadgeLedger: ${e.message}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModalView(
|
||||
close,
|
||||
cardScreen = true,
|
||||
endButtons = {
|
||||
val loaded = entries.value
|
||||
if (!loaded.isNullOrEmpty()) {
|
||||
ShareButton { clipboard.shareText(ledgerShareText(loaded)) }
|
||||
}
|
||||
}
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.badges_ledger))
|
||||
val loaded = entries.value
|
||||
if (loaded != null) {
|
||||
SectionView {
|
||||
if (loaded.isEmpty()) {
|
||||
SectionItemView {
|
||||
Text(stringResource(MR.strings.badges_ledger_no_entries), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
} else {
|
||||
loaded.forEach { LedgerRow(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LedgerRow(entry: StatementEntry) {
|
||||
val expanded = remember(entry.entryId) { mutableStateOf(false) }
|
||||
SectionItemView(click = { expanded.value = !expanded.value }) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(entry.entryType.text)
|
||||
Text(localDate(entry.createdAt), color = MaterialTheme.colors.secondary, fontSize = 12.sp)
|
||||
}
|
||||
Text(changeText(entry), color = MaterialTheme.colors.secondary)
|
||||
Icon(
|
||||
painterResource(if (expanded.value) MR.images.ic_chevron_up else MR.images.ic_chevron_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (expanded.value) {
|
||||
val indented = PaddingValues(start = 24.dp + itemHPadding, end = itemHPadding)
|
||||
entryFields(entry).forEach { (label, value) -> InfoRow(label, value, padding = indented) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeText(entry: StatementEntry): String {
|
||||
val n = entry.changeMonths
|
||||
val months = String.format(generalGetString(if (abs(n) == 1) MR.strings.ttl_month else MR.strings.ttl_months), n)
|
||||
return if (n > 0) "+$months" else months
|
||||
}
|
||||
|
||||
private fun entryFields(entry: StatementEntry): List<Pair<String, String>> {
|
||||
val fields = mutableListOf(
|
||||
generalGetString(MR.strings.badges_ledger_date) to localTimestamp(entry.createdAt),
|
||||
generalGetString(MR.strings.badges_ledger_balance) to entry.balanceMonths.toString(),
|
||||
generalGetString(MR.strings.badges_ledger_balance_start) to localTimestamp(entry.balanceStartTs),
|
||||
generalGetString(MR.strings.badges_ledger_anchor) to localTimestamp(entry.balanceAnchorTs),
|
||||
generalGetString(MR.strings.badges_ledger_badge_type) to entry.balanceBadgeType.text,
|
||||
)
|
||||
val pausedSince = entry.wasPausedSince
|
||||
if (pausedSince != null) {
|
||||
fields.add(generalGetString(MR.strings.badges_ledger_paused_since) to localTimestamp(pausedSince))
|
||||
}
|
||||
fields.add(generalGetString(MR.strings.badges_ledger_entry_id) to entry.entryId)
|
||||
payloadField(entry.entryType)?.let { fields.add(it) }
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun payloadField(entryType: StatementEntryType): Pair<String, String>? = when (entryType) {
|
||||
is StatementEntryType.Credit -> when (val credit = entryType.credit) {
|
||||
is StatementCreditType.Payment -> credit.invoiceId?.let { generalGetString(MR.strings.badges_ledger_invoice_id) to it }
|
||||
is StatementCreditType.Charge -> generalGetString(MR.strings.badges_ledger_charge_id) to credit.chargeId
|
||||
is StatementCreditType.TransferIn -> generalGetString(MR.strings.badges_ledger_from_purchase_key) to credit.fromPurchaseKey
|
||||
is StatementCreditType.Code, is StatementCreditType.Support, is StatementCreditType.Opening, is StatementCreditType.Unknown -> null
|
||||
}
|
||||
is StatementEntryType.Debit -> when (val debit = entryType.debit) {
|
||||
is StatementDebitType.Upgrade -> generalGetString(MR.strings.badges_ledger_to_purchase_key) to debit.toPurchaseKey
|
||||
is StatementDebitType.TransferOut -> generalGetString(MR.strings.badges_ledger_to_purchase_key) to debit.toPurchaseKey
|
||||
is StatementDebitType.Refund, is StatementDebitType.Support, is StatementDebitType.Badge, is StatementDebitType.Lapse, is StatementDebitType.Unknown -> null
|
||||
}
|
||||
}
|
||||
|
||||
// the JSON as core sent it: English field names and ISO dates, for support
|
||||
private fun ledgerShareText(entries: List<StatementEntry>): String = json.encodeToString(entries)
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum class BadgePeriod {
|
||||
OneMonth,
|
||||
Monthly,
|
||||
Annual;
|
||||
|
||||
val icon: dev.icerock.moko.resources.ImageResource
|
||||
get() = when (this) {
|
||||
OneMonth -> MR.images.ic_calendar
|
||||
Monthly -> MR.images.ic_refresh
|
||||
Annual -> MR.images.ic_refresh
|
||||
}
|
||||
|
||||
val label: StringResource
|
||||
get() = when (this) {
|
||||
OneMonth -> MR.strings.badges_period_one_month
|
||||
Monthly -> MR.strings.badges_period_monthly
|
||||
Annual -> MR.strings.badges_period_annual
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun priceText(price: BadgePrice): String = when (price) {
|
||||
is BadgePrice.Loading -> "…"
|
||||
is BadgePrice.Unavailable -> "—"
|
||||
is BadgePrice.Price -> when (this) {
|
||||
OneMonth -> price.price
|
||||
Monthly -> stringResource(MR.strings.badges_price_monthly).format(price.price)
|
||||
Annual -> stringResource(MR.strings.badges_price_annual).format(price.price)
|
||||
}
|
||||
}
|
||||
|
||||
fun payLabel(price: BadgePrice): Pair<StringResource, String?> = when (price) {
|
||||
is BadgePrice.Loading -> MR.strings.badges_price_loading to null
|
||||
is BadgePrice.Unavailable -> MR.strings.badges_price_unavailable to null
|
||||
is BadgePrice.Price -> when (this) {
|
||||
OneMonth -> MR.strings.badges_pay_once to price.price
|
||||
Monthly -> MR.strings.badges_pay_monthly to price.price
|
||||
Annual -> MR.strings.badges_pay_annual to price.price
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgesPayView(level: BadgeLevel) {
|
||||
var selectedPeriod by remember { mutableStateOf(BadgePeriod.Monthly) }
|
||||
val purchasing = remember { mutableStateOf(false) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
LaunchedEffect(Unit) { BadgeStore.load() }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(level.title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BadgeUserPreview(level = level, modifier = Modifier.padding(top = 4.dp))
|
||||
|
||||
Text(
|
||||
stringResource(level.tagline),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 8.dp))
|
||||
|
||||
// IntrinsicSize.Max + fillMaxHeight on children so all three cards match the tallest one -
|
||||
// only Annual carries a savings line, and prices wrap at large fonts.
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
PeriodCard(level, BadgePeriod.OneMonth, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
PeriodCard(level, BadgePeriod.Monthly, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
PeriodCard(level, BadgePeriod.Annual, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 8.dp))
|
||||
|
||||
// Replicates TextButtonBelowOnboardingButton spacing (7.5dp outer + 5dp inner) without a
|
||||
// TextButton so the footer has no hover/click affordance.
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PayButton(level, selectedPeriod, purchasing, clipboard)
|
||||
Box(Modifier.padding(top = 7.5.dp, bottom = 7.5.dp).padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Text(
|
||||
stringResource(billingFooter(selectedPeriod)).format(stubBillingDate()),
|
||||
Modifier.padding(vertical = 5.dp),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PeriodCard(level: BadgeLevel, period: BadgePeriod, selectedPeriod: BadgePeriod, modifier: Modifier, onSelect: (BadgePeriod) -> Unit) {
|
||||
val isSelected = period == selectedPeriod
|
||||
val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f)
|
||||
// Light: transparent so card matches page background. Dark: subtle gray tint for visible contrast.
|
||||
val cardBackground = if (isInDarkTheme()) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
else MaterialTheme.colors.background
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Column(
|
||||
modifier
|
||||
.clip(shape)
|
||||
.background(cardBackground, shape)
|
||||
.border(2.dp, borderColor, shape)
|
||||
.clickable { onSelect(period) }
|
||||
.padding(vertical = 20.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(period.icon),
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
Text(period.priceText(BadgeStore.price(level, period)), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
|
||||
val percent = savingsPercent(level, period)
|
||||
if (percent != null) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_savings).format("${percent}%"),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun savingsPercent(level: BadgeLevel, period: BadgePeriod): Int? =
|
||||
if (period == BadgePeriod.Annual) BadgeStore.annualSavings(level) else null
|
||||
|
||||
// TODO [badges] on desktop and the foss build there is no store, so every price is Unavailable and
|
||||
// this button stays disabled - it will offer Stripe/crypto payment instead
|
||||
@Composable
|
||||
private fun PayButton(level: BadgeLevel, selectedPeriod: BadgePeriod, purchasing: MutableState<Boolean>, clipboard: ClipboardManager) {
|
||||
val price = BadgeStore.price(level, selectedPeriod)
|
||||
val (labelId, labelArg) = selectedPeriod.payLabel(price)
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = labelId,
|
||||
labelArg = labelArg,
|
||||
onboarding = null,
|
||||
enabled = price.canPurchase && !purchasing.value,
|
||||
onclick = { purchase(level, selectedPeriod, purchasing, clipboard) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun purchase(level: BadgeLevel, period: BadgePeriod, purchasing: MutableState<Boolean>, clipboard: ClipboardManager) {
|
||||
val invoiceId = newBadgeInvoiceId()
|
||||
purchasing.value = true
|
||||
// not withBGApi: the purchase waits for the user in the Play sheet and would block chat API calls
|
||||
withLongRunningApi {
|
||||
try {
|
||||
val outcome = BadgeStore.purchase(level, period, invoiceId)
|
||||
purchasing.value = false
|
||||
when (outcome) {
|
||||
is BadgePurchaseOutcome.Purchased -> showPurchasedAlert(outcome.receipt, invoiceId, clipboard)
|
||||
is BadgePurchaseOutcome.Pending -> AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_purchase_pending),
|
||||
text = generalGetString(MR.strings.badges_purchase_pending_desc)
|
||||
)
|
||||
is BadgePurchaseOutcome.Cancelled -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "BadgesPayView.purchase: ${e.stackTraceToString()}")
|
||||
purchasing.value = false
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_purchase_error),
|
||||
text = e.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [badges] store integration diagnostics - replaced by the issued badge once the service lands.
|
||||
private fun showPurchasedAlert(receipt: BadgeStoreReceipt, invoiceId: String, clipboard: ClipboardManager) {
|
||||
val returnedInvoice = when (receipt.invoiceId) {
|
||||
null -> "none"
|
||||
invoiceId -> "yes"
|
||||
else -> "mismatch: ${receipt.invoiceId}"
|
||||
}
|
||||
val lines = mutableListOf(
|
||||
"Product: ${receipt.productId}",
|
||||
"Invoice: $invoiceId",
|
||||
"Invoice returned by Google: $returnedInvoice",
|
||||
"Order: ${receipt.orderId ?: "none"}"
|
||||
)
|
||||
if (receipt.environment != null) lines.add("Environment: ${receipt.environment}")
|
||||
lines.add("Token: ${receipt.token.length} bytes")
|
||||
val summary = lines.joinToString("\n")
|
||||
Log.d(TAG, "badge purchase succeeded\n$summary")
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.badges_purchase_successful),
|
||||
text = summary,
|
||||
confirmText = "Copy token",
|
||||
onConfirm = {
|
||||
clipboard.setText(AnnotatedString(receipt.token))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
},
|
||||
dismissText = generalGetString(MR.strings.ok),
|
||||
parseHtml = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun billingFooter(period: BadgePeriod): StringResource = when (period) {
|
||||
BadgePeriod.Monthly, BadgePeriod.Annual -> MR.strings.badges_billing_footer_subscribe
|
||||
BadgePeriod.OneMonth -> MR.strings.badges_billing_footer_one_month
|
||||
}
|
||||
|
||||
// TODO [badges] source the actual date from the purchase state machine when wired.
|
||||
private fun stubBillingDate(): String {
|
||||
val date = java.time.LocalDate.of(2026, 7, 22)
|
||||
val formatter = java.time.format.DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.LONG)
|
||||
return date.format(formatter)
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
private const val badgeCodePrefix = "SB"
|
||||
private const val badgeCodeBodyLength = 20
|
||||
private const val badgeCodeGroupLength = 5
|
||||
|
||||
// Regroups what was typed; validity and the folding of ambiguous characters are core's alone.
|
||||
private fun formatBadgeCodeInput(s: String): String {
|
||||
val normalized = StringBuilder()
|
||||
for (c in s.take(256).uppercase()) {
|
||||
if (!c.isLetterOrDigit()) continue
|
||||
normalized.append(c)
|
||||
if (normalized.length == badgeCodePrefix.length + badgeCodeBodyLength) break
|
||||
}
|
||||
if (!normalized.startsWith(badgeCodePrefix)) return normalized.toString()
|
||||
val groups = mutableListOf(badgeCodePrefix)
|
||||
var i = badgeCodePrefix.length
|
||||
while (i < normalized.length) {
|
||||
val j = minOf(i + badgeCodeGroupLength, normalized.length)
|
||||
groups.add(normalized.substring(i, j))
|
||||
i = j
|
||||
}
|
||||
return groups.joinToString("-")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgesRedeemCodeView() {
|
||||
val rhId = remember { chatModel.remoteHostId() }
|
||||
val supporterBannerShown = remember { appPrefs.supporterBannerShown }
|
||||
val code = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val canonicalCode = remember { mutableStateOf<String?>(null) }
|
||||
val submitting = remember { mutableStateOf(false) }
|
||||
|
||||
// when the text is unchanged, the field's own value is kept: it carries the cursor position and the
|
||||
// keyboard's composition state, which BasicTextField loses unless they are passed back to it
|
||||
fun applyCodeInput(v: TextFieldValue) {
|
||||
val formatted = formatBadgeCodeInput(v.text)
|
||||
code.value = if (formatted != v.text) TextFieldValue(formatted, selection = TextRange(formatted.length)) else v
|
||||
canonicalCode.value = parseBadgeCode(formatted)
|
||||
}
|
||||
|
||||
fun redeem() {
|
||||
val sending = canonicalCode.value ?: return
|
||||
val user = chatModel.currentUser.value ?: return
|
||||
submitting.value = true
|
||||
withBGApi {
|
||||
when (val result = chatModel.controller.apiRedeemBadgeCode(rhId, user.userId, sending)) {
|
||||
null -> withContext(Dispatchers.Main) { submitting.value = false }
|
||||
is BadgeRedeemResult.Redeemed -> {
|
||||
val badgeState = result.badgeState
|
||||
withContext(Dispatchers.Main) {
|
||||
submitting.value = false
|
||||
// set before dismissing: BadgesView then switches Support to Your Badge while this screen
|
||||
// still covers it, so the pop lands on Your Badge instead of showing the switch
|
||||
BadgeModel.set(rhId, user.userId, badgeState)
|
||||
chatModel.updateUser(result.user)
|
||||
if (badgeState != null && !badgeState.shown) {
|
||||
// a replay adds no purchase; a fresh code's badge can be retired on arrival
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_error_title),
|
||||
text = generalGetString(if (result.newBadge) MR.strings.badges_error_badge_ended else MR.strings.badges_error_code_used)
|
||||
)
|
||||
} else {
|
||||
supporterBannerShown.set(true)
|
||||
ModalManager.start.closeModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
is BadgeRedeemResult.Failed -> {
|
||||
Log.e(TAG, "apiRedeemBadgeCode: ${result.err?.string}")
|
||||
withContext(Dispatchers.Main) {
|
||||
submitting.value = false
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_error_title),
|
||||
text = chatModel.controller.redeemErrorText(result.err)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_redeem_code_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(MR.strings.badges_redeem_code_body),
|
||||
style = MaterialTheme.typography.body1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
CodeField(code, submitting.value, ::applyCodeInput)
|
||||
|
||||
PasteButton(submitting.value, ::applyCodeInput)
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
QRCodeScanner(padding = PaddingValues(start = 16.dp, top = 12.dp, end = 16.dp)) { text ->
|
||||
val formatted = formatBadgeCodeInput(text)
|
||||
when {
|
||||
submitting.value -> false
|
||||
parseBadgeCode(formatted) == null -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_qr_code),
|
||||
text = generalGetString(MR.strings.badges_code_you_scanned_is_not_badge_code)
|
||||
)
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
applyCodeInput(TextFieldValue(formatted, selection = TextRange(formatted.length)))
|
||||
redeem()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SubmitButton(enabled = canonicalCode.value != null && !submitting.value, onClick = ::redeem)
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
}
|
||||
|
||||
if (submitting.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){}
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CodeField(code: MutableState<TextFieldValue>, submitting: Boolean, applyCodeInput: (TextFieldValue) -> Unit) {
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = MaterialTheme.appColors.sentMessage,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
BasicTextField(
|
||||
value = code.value,
|
||||
onValueChange = applyCodeInput,
|
||||
enabled = !submitting,
|
||||
singleLine = true,
|
||||
textStyle = TextStyle.Default.copy(
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = MaterialTheme.typography.body1.fontSize,
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Characters, autoCorrect = false),
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(colors.backgroundColor(!submitting).value)
|
||||
.padding(start = 12.dp, top = 14.dp, end = 12.dp, bottom = 14.dp),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (code.value.text.isEmpty()) {
|
||||
Text(stringResource(MR.strings.badges_redeem_code_placeholder), color = MaterialTheme.colors.secondary, fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasteButton(submitting: Boolean, applyCodeInput: (TextFieldValue) -> Unit) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
TextButton(
|
||||
onClick = { clipboard.getText()?.text?.let { applyCodeInput(TextFieldValue(it, selection = TextRange(it.length))) } },
|
||||
enabled = !submitting
|
||||
) {
|
||||
Text(stringResource(MR.strings.paste_button), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubmitButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_redeem,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = onClick
|
||||
)
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.common.views.onboarding.HowItWorks
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgesSupportSimplexView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_support_simplex_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(MR.strings.badges_support_simplex_body),
|
||||
style = MaterialTheme.typography.body1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// TODO [badges] restore WhyBuiltButton() when in-app purchase lands: the level screen
|
||||
// returns to the flow and HowItWorksButton() moves there, leaving this one alone here.
|
||||
HowItWorksButton()
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
PhoneSupporterHero(Modifier.fillMaxWidth(0.55f))
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
RedeemCodeButton()
|
||||
GetCodeButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the in-app purchase path, kept compiling and uncalled until payments return after the MVP
|
||||
@Composable
|
||||
private fun ChooseLevelButton() {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_choose_your_level,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
ModalManager.start.showModal { BadgesYourLevelView() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WhyBuiltButton() {
|
||||
val primary = MaterialTheme.colors.primary
|
||||
TextButton({
|
||||
ModalManager.start.showModal { HowItWorks(user = chatModel.currentUser.value, onboardingStage = null, titleColor = primary) }
|
||||
}) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
|
||||
Text(stringResource(MR.strings.badges_why_simplex_is_built), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowItWorksButton() {
|
||||
TextButton({
|
||||
ModalManager.start.showModal { BadgesHowItWorksView() }
|
||||
}) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_button), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RedeemCodeButton() {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_redeem_code_button,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
ModalManager.start.showModal { BadgesRedeemCodeView() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GetCodeButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
TextButtonBelowOnboardingButton(
|
||||
text = stringResource(MR.strings.badges_get_your_code),
|
||||
onClick = { uriHandler.openExternalLink("https://simplex.chat/badges/") }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhoneSupporterHero(modifier: Modifier = Modifier) {
|
||||
val isDark = isInDarkTheme()
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
val scale = if (isDark) 1.5f else 1.2f
|
||||
val brush = remember(size, isDark) {
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
val aspect = size.height.toFloat() / size.width.toFloat()
|
||||
val gp = gradientPoints(aspect, scale)
|
||||
Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(gp.startX * size.width, gp.startY * size.height),
|
||||
end = Offset(gp.endX * size.width, gp.endY * size.height)
|
||||
)
|
||||
} else {
|
||||
Brush.linearGradient(colorStops = stops)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(brush)
|
||||
.onSizeChanged { size = it },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.BadgeModel
|
||||
import chat.simplex.common.model.BadgeState
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.ModalView
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun BadgesView(close: () -> Unit) {
|
||||
val shownBadge: BadgeState? = run {
|
||||
if (!BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)) return@run null
|
||||
val badgeState = BadgeModel.badgeState.value
|
||||
if (badgeState != null && badgeState.shown) badgeState else null
|
||||
}
|
||||
|
||||
// the card look is a modal setting, so the modal is composed here to follow the screen shown
|
||||
ModalView(close, cardScreen = shownBadge != null) {
|
||||
AnimatedContent(targetState = shownBadge, transitionSpec = { fadeIn() with fadeOut() }, contentKey = { it != null }) { badgeState ->
|
||||
if (badgeState != null) {
|
||||
BadgesYourBadgeView(badgeState)
|
||||
} else {
|
||||
BadgesSupportSimplexView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import InfoRow
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeState
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.localTimestamp
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import chat.simplex.common.views.helpers.badgeImage
|
||||
import chat.simplex.common.views.helpers.badgeTypeName
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgesYourBadgeView(badgeState: BadgeState) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.badges_your_badge))
|
||||
|
||||
SectionView {
|
||||
BadgeSummary(badgeState)
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.badges_ends)) {
|
||||
Text(badgeState.paidThroughText, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 12.dp))
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.badges_prepaid_footer))
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.badges_how_it_works_button),
|
||||
{ ModalManager.start.showModal { BadgesHowItWorksView() } },
|
||||
)
|
||||
}
|
||||
SectionSpacer()
|
||||
if (appPrefs.developerTools.get()) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionView(stringResource(MR.strings.badges_credential)) {
|
||||
val badge = chatModel.currentUser.value?.profile?.localBadge
|
||||
if (badge != null) {
|
||||
InfoRow(stringResource(MR.strings.badges_credential_status), badge.status.name)
|
||||
InfoRow(stringResource(MR.strings.badges_credential_expires), localTimestamp(badge.badge.badgeExpiry))
|
||||
}
|
||||
InfoRow(stringResource(MR.strings.badges_credential_months_left), badgeState.monthsLeft.toString())
|
||||
InfoRow(stringResource(MR.strings.badges_credential_purchase_id), badgeState.badgePurchaseId.toString())
|
||||
SectionItemView({ clipboard.setText(AnnotatedString(badgeState.purchaseKey)) }) {
|
||||
Text(stringResource(MR.strings.badges_copy_purchase_key), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({ ModalManager.start.showCustomModal { close -> BadgesLedgerView(badgeState, close) } }) {
|
||||
Text(stringResource(MR.strings.badges_ledger))
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgeSummary(badgeState: BadgeState) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Image(
|
||||
painterResource(badgeImage(badgeState.badgeType)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.padding(bottom = 8.dp).size(68.dp)
|
||||
)
|
||||
|
||||
Text(badgeTypeName(badgeState.badgeType), style = MaterialTheme.typography.h3, fontWeight = FontWeight.SemiBold)
|
||||
|
||||
Text(
|
||||
stringResource(MR.strings.badges_shown_on_your_profile),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeType
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum class BadgeLevel {
|
||||
Supporter,
|
||||
Legend;
|
||||
|
||||
val title: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter
|
||||
Legend -> MR.strings.badges_level_legend
|
||||
}
|
||||
|
||||
val filesDescription: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_files
|
||||
Legend -> MR.strings.badges_level_legend_files
|
||||
}
|
||||
|
||||
val tagline: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_tagline
|
||||
Legend -> MR.strings.badges_level_legend_tagline
|
||||
}
|
||||
|
||||
val badgeType: BadgeType
|
||||
get() = when (this) {
|
||||
Supporter -> BadgeType.Supporter
|
||||
Legend -> BadgeType.Legend
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgesYourLevelView() {
|
||||
var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) }
|
||||
|
||||
LaunchedEffect(Unit) { BadgeStore.load() }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_your_level_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BadgeUserPreview(level = selectedLevel, modifier = Modifier.padding(top = 4.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// IntrinsicSize.Max + fillMaxHeight on children so both cards match the taller card's height
|
||||
// when 2-line labels at large fonts would otherwise size them differently.
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
LevelCard(BadgeLevel.Supporter, selectedLevel, Modifier.weight(1f).fillMaxHeight()) { selectedLevel = it }
|
||||
LevelCard(BadgeLevel.Legend, selectedLevel, Modifier.weight(1f).fillMaxHeight()) { selectedLevel = it }
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// Nested Column with no spacing so the TextButtonBelowOnboardingButton sits directly under
|
||||
// the action button (matches onboarding pattern where its own 7.5dp top padding is the gap).
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
ContinueButton(selectedLevel)
|
||||
TextButtonBelowOnboardingButton(
|
||||
text = stringResource(MR.strings.badges_how_it_works_button),
|
||||
icon = painterResource(MR.images.ic_info),
|
||||
onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Modifier, onSelect: (BadgeLevel) -> Unit) {
|
||||
val isSelected = level == selectedLevel
|
||||
val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f)
|
||||
// Light: transparent so card matches page background. Dark: subtle gray tint for visible contrast.
|
||||
val cardBackground = if (isInDarkTheme()) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
else MaterialTheme.colors.background
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Column(
|
||||
modifier
|
||||
.clip(shape)
|
||||
.background(cardBackground, shape)
|
||||
.border(2.dp, borderColor, shape)
|
||||
.clickable { onSelect(level) }
|
||||
.padding(vertical = 20.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Image(
|
||||
painterResource(badgeImage(level.badgeType)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(60.dp)
|
||||
)
|
||||
Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
Text(BadgePeriod.Monthly.priceText(BadgeStore.price(level, BadgePeriod.Monthly)), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(selectedLevel: BadgeLevel) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_continue,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
ModalManager.start.showModal { BadgesPayView(selectedLevel) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun SupportSimpleXBanner(
|
||||
title: String = generalGetString(MR.strings.badges_banner_title),
|
||||
subtitle: String = generalGetString(MR.strings.badges_banner_subtitle),
|
||||
onTap: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val cardCornerRadius = 16.dp
|
||||
// grows linearly with system font but never shrinks below the default so small-font users see the
|
||||
// same baseline; the card Row uses heightIn(min = cardHeight) and grows further when 2-line text
|
||||
// wraps at very large fonts. Hero stays fixed so its above-card overhang shrinks at very large fonts.
|
||||
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
|
||||
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
|
||||
val cardLeadingPadding = 16.dp
|
||||
val cardTrailingPadding = 8.dp
|
||||
val heroWidth = 110.dp
|
||||
// shorter than the natural drawn height so ContentScale.Crop slices the phone body at card bottom
|
||||
val heroVisibleHeight = 108.dp
|
||||
// hero right edge sits at the dismiss X's icon left edge (X: outer 4pt + inner-pad 8 + half of 16pt icon)
|
||||
val heroTrailingPadding = 28.dp
|
||||
val textToHeroGap = 6.dp
|
||||
|
||||
val isDark = isInDarkTheme()
|
||||
var cardSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
|
||||
|
||||
// Layout sizes to the card; hero is placed at y = cardHeight - heroHeight (negative → hero
|
||||
// overhangs above card at normal fonts, 0/positive → hero fits inside card at large fonts).
|
||||
Layout(content = {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cardHeight)
|
||||
.clip(RoundedCornerShape(cardCornerRadius))
|
||||
.background(brush)
|
||||
.clickable(onClick = onTap)
|
||||
.onSizeChanged { cardSize = it }
|
||||
.padding(
|
||||
start = cardLeadingPadding,
|
||||
end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap,
|
||||
top = 12.dp,
|
||||
bottom = 12.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
subtitle,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_close_button),
|
||||
tint = if (isDark) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onDismiss)
|
||||
.padding(8.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
HeroThumbnail(
|
||||
heroWidth = heroWidth,
|
||||
heroVisibleHeight = heroVisibleHeight,
|
||||
cardHeight = cardHeight,
|
||||
trailingPadding = heroTrailingPadding
|
||||
)
|
||||
}) { measurables, constraints ->
|
||||
val cardPlaceable = measurables[0].measure(constraints)
|
||||
val heroPlaceable = measurables[1].measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
layout(cardPlaceable.width, cardPlaceable.height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
heroPlaceable.place(cardPlaceable.width - heroPlaceable.width, cardPlaceable.height - heroPlaceable.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroThumbnail(heroWidth: Dp, heroVisibleHeight: Dp, cardHeight: Dp, trailingPadding: Dp) {
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
// draws at natural aspect, top-aligned in a shorter slot; ContentScale.Crop cuts the overflow at card bottom
|
||||
Image(
|
||||
painterResource(if (isInDarkTheme()) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
modifier = Modifier.padding(end = trailingPadding).size(width = heroWidth, height = heroVisibleHeight)
|
||||
)
|
||||
} else {
|
||||
val badgeSize = 48.dp
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.padding(end = trailingPadding + 12.dp, top = (cardHeight - badgeSize) / 2, bottom = (cardHeight - badgeSize) / 2)
|
||||
.size(badgeSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry-aware gradient with asymmetric scale: start (dark) pushed further below the card than
|
||||
// end (warm) is above, so card-middle lands at the bright/mid-transition stop, not the dark region.
|
||||
private fun gradientBrush(isDark: Boolean, size: IntSize): Brush {
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
if (size.width == 0 || size.height == 0) return Brush.linearGradient(colorStops = stops)
|
||||
val w = size.width.toFloat()
|
||||
val h = size.height.toFloat()
|
||||
val startScale = if (isDark) 3.0f else 2.5f
|
||||
val endScale = if (isDark) 2.1f else 1.7f
|
||||
val gp = gradientPoints(h / w, 1.0f)
|
||||
val sx = 0.5f + (gp.startX - 0.5f) * startScale
|
||||
val sy = 0.5f + (gp.startY - 0.5f) * startScale
|
||||
val ex = 0.5f + (gp.endX - 0.5f) * endScale
|
||||
val ey = 0.5f + (gp.endY - 0.5f) * endScale
|
||||
return Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(sx * w, sy * h),
|
||||
end = Offset(ex * w, ey * h)
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -502,7 +502,7 @@ fun ChatView(
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (updatedMember, code) = if (member.memberActive) {
|
||||
val (updatedMember, code) = if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent)) && member.memberRole != GroupMemberRole.Relay) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
(memCode?.first ?: r?.first ?: member) to memCode?.second
|
||||
} else {
|
||||
|
||||
+8
-4
@@ -115,7 +115,8 @@ data class ComposeState(
|
||||
val useLinkPreviews: Boolean,
|
||||
val mentions: MentionedMembers = emptyMap(),
|
||||
// the max file size the user may attach, raised by their active badge unless the chat is incognito; kept in sync on chat switch
|
||||
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP)
|
||||
val maxFileSize: Long = getMaxFileSize(FileProtocol.XFTP),
|
||||
val sendIncognito: Boolean = false
|
||||
) {
|
||||
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
|
||||
ComposeMessage(
|
||||
@@ -331,7 +332,7 @@ fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
} else if (fileSize != null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
largeFileMessage(fileSize, value.sendIncognito, expiredBadgeReason(fileSize, chatModel.currentUser.value?.profile))
|
||||
)
|
||||
} else {
|
||||
showWrongUriAlert()
|
||||
@@ -360,7 +361,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
largeFileMessage(fileSize ?: 0, value.sendIncognito, expiredBadgeReason(fileSize ?: 0, chatModel.currentUser.value?.profile))
|
||||
)
|
||||
null
|
||||
}
|
||||
@@ -1420,7 +1421,10 @@ fun ComposeView(
|
||||
// keep the attach size limit in sync with the chat: the user's active badge raises it, but not in incognito chats where no badge is presented
|
||||
LaunchedEffect(chat.chatInfo) {
|
||||
val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get()
|
||||
composeState.value = composeState.value.copy(maxFileSize = getMaxFileSize(FileProtocol.XFTP, if (incognito) null else chatModel.currentUser.value?.profile))
|
||||
composeState.value = composeState.value.copy(
|
||||
maxFileSize = getMaxFileSize(FileProtocol.XFTP, if (incognito) null else chatModel.currentUser.value?.profile),
|
||||
sendIncognito = incognito
|
||||
)
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// the same ComposeView is reused when switching chats, so `chat` captured by onDispose would be the chat opened first, not the current one
|
||||
|
||||
+20
-11
@@ -38,7 +38,6 @@ fun CIFileView(
|
||||
showTimestamp: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean = false,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
|
||||
@@ -77,13 +76,11 @@ fun CIFileView(
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
val prohibited = file.fileProhibited
|
||||
if (prohibited != null) {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
@@ -157,7 +154,7 @@ fun CIFileView(
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (!fileSizeValid(file, senderProfile))
|
||||
if (!fileSizeValid(file))
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
|
||||
else if (file.expired)
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
@@ -239,9 +236,21 @@ fun CIFileView(
|
||||
}
|
||||
}
|
||||
|
||||
// whether a received file is within the size we accept from its sender
|
||||
fun fileSizeValid(file: CIFile, senderProfile: LocalProfile?): Boolean =
|
||||
file.fileSize <= getMaxFileSize(file.fileProtocol, senderProfile)
|
||||
// the core decides whether a received file is above the size the sender's badge allows
|
||||
fun fileSizeValid(file: CIFile): Boolean = file.fileProhibited == null
|
||||
|
||||
fun showProhibitedFileAlert(file: CIFile, prohibited: FileProhibited) {
|
||||
val badgeIssue = when (prohibited.badgeStatus) {
|
||||
null, BadgeStatus.Active -> ""
|
||||
BadgeStatus.Expired, BadgeStatus.ExpiredOld -> generalGetString(MR.strings.badge_expired)
|
||||
BadgeStatus.Failed -> generalGetString(MR.strings.badge_verification_failed)
|
||||
BadgeStatus.UnknownKey -> generalGetString(MR.strings.badge_no_key)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
largeFileMessage(file.fileSize, badgeIssue = badgeIssue)
|
||||
)
|
||||
}
|
||||
|
||||
fun showFileErrorAlert(err: FileError, file: CIFile? = null, temporary: Boolean = false) {
|
||||
val fileExpires = file?.fileExpires
|
||||
|
||||
+4
-8
@@ -38,7 +38,6 @@ fun CIImageView(
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
@@ -84,7 +83,7 @@ fun CIImageView(
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
if (file.expired && fileSizeValid(file))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
@@ -220,13 +219,10 @@ fun CIImageView(
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
|
||||
+10
-14
@@ -35,7 +35,6 @@ fun CIVideoView(
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean = false,
|
||||
senderProfile: LocalProfile?,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
@@ -99,7 +98,7 @@ fun CIVideoView(
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
receiveFileIfValidSize(file, senderProfile, receiveFile)
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
@@ -129,16 +128,16 @@ fun CIVideoView(
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) {
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, senderProfile, receiveFile) }
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not show download icon when the view is blurred
|
||||
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
|
||||
fileStatusIcon(file, false, senderProfile)
|
||||
fileStatusIcon(file, false)
|
||||
} else if (smallView && file?.showStatusIconInSmallView == true) {
|
||||
Box(Modifier.align(Alignment.Center)) {
|
||||
fileStatusIcon(file, true, senderProfile)
|
||||
fileStatusIcon(file, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +485,7 @@ private fun progressCircle(progress: Long, total: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: LocalProfile?) {
|
||||
private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -526,7 +525,7 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: Loc
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (file.expired && fileSizeValid(file, senderProfile))
|
||||
if (file.expired && fileSizeValid(file))
|
||||
fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
else
|
||||
fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
@@ -565,14 +564,11 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean, senderProfile: Loc
|
||||
private fun showDownloadButton(status: CIFileStatus?): Boolean =
|
||||
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
|
||||
|
||||
private fun receiveFileIfValidSize(file: CIFile, senderProfile: LocalProfile?, receiveFile: (Long) -> Unit) {
|
||||
if (fileSizeValid(file, senderProfile)) {
|
||||
receiveFile(file.fileId)
|
||||
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol, senderProfile)))
|
||||
)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -449,7 +449,7 @@ fun ChatItemView(
|
||||
}
|
||||
if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) {
|
||||
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
|
||||
} else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file, ciSenderProfile(cItem, chat.chatInfo))) {
|
||||
} else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) {
|
||||
ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = {
|
||||
withBGApi {
|
||||
Log.d(TAG, "ChatItemView downloadFileAction")
|
||||
|
||||
+3
-3
@@ -219,7 +219,7 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
@@ -364,7 +364,7 @@ fun FramedItemView(
|
||||
} else {
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
@@ -372,7 +372,7 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, senderProfile = ciSenderProfile(ci, chatInfo), receiveFile = receiveFile)
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, receiveFile = receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -622,7 +622,7 @@ fun stripFormattedTextLink(ft: List<FormattedText>?, link: String): List<Formatt
|
||||
val i = result.lastIndex
|
||||
if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) {
|
||||
result[i] = FormattedText(result[i].text.dropLast(1), null)
|
||||
if (result[i].text.isEmpty()) result.removeLast()
|
||||
if (result[i].text.isEmpty()) result.removeAt(result.lastIndex)
|
||||
}
|
||||
return result.ifEmpty { null }
|
||||
}
|
||||
|
||||
+75
@@ -36,11 +36,13 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import SectionItemView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.badges.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -62,6 +64,41 @@ sealed class ActiveFilter {
|
||||
data object Unread: ActiveFilter()
|
||||
}
|
||||
|
||||
private fun showSupportEndedDismissAlert() {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.badges_support_ended),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withBGApi { chatModel.controller.ackBadgeAlert(snooze = true) }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.badges_remind_me_later), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withBGApi { chatModel.controller.ackBadgeAlert(snooze = false) }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.badges_dismiss), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showSupportSimpleXDismissAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_banner_title),
|
||||
text = generalGetString(MR.strings.badges_banner_dismiss_message),
|
||||
onConfirm = { appPrefs.supporterBannerShown.set(true) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
ModalManager.start.closeModals()
|
||||
@@ -912,6 +949,11 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||
val supporterBannerShown = remember { appPrefs.supporterBannerShown.state }
|
||||
val getStakeBannerTapped = remember { appPrefs.getStakeBannerTapped.state }
|
||||
val getStakeBannerDismissed = remember { appPrefs.getStakeBannerDismissed.state }
|
||||
// read here rather than in the LazyColumn: it launches an effect, so it needs a composable scope
|
||||
val crowdfunding = crowdfundingAvailable()
|
||||
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
@@ -1000,6 +1042,39 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
ToggleChatListCard()
|
||||
}
|
||||
}
|
||||
// one slot: a badge the user paid for ending outranks the pitch to get one
|
||||
val alert = BadgeModel.alert.value
|
||||
if (supportEnded() && alert != null) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
title = stringResource(MR.strings.badges_support_ended),
|
||||
subtitle = String.format(stringResource(MR.strings.badges_support_ended_on), alert.dateText),
|
||||
onTap = { ModalManager.start.showCustomModal { close -> BadgesView(close) } },
|
||||
onDismiss = ::showSupportEndedDismissAlert
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (!supporterBannerShown.value && !hasShownBadge() && chatModel.chats.value.size > 3) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
onTap = { ModalManager.start.showCustomModal { close -> BadgesView(close) } },
|
||||
onDismiss = ::showSupportSimpleXDismissAlert
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (crowdfunding && !getStakeBannerDismissed.value) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = getStakeBannerTapped.value && chatModel.chats.value.isNotEmpty(),
|
||||
onTap = { openGetStake(ModalManager.start) },
|
||||
onDismiss = { appPrefs.getStakeBannerDismissed.set(true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
|
||||
+3
-3
@@ -324,13 +324,13 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCImage -> SmallContentPreview {
|
||||
CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIImageView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> SmallContentPreview {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIVideoView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
@@ -342,7 +342,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> SmallContentPreviewFile {
|
||||
CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true) {
|
||||
val user = chatModel.currentUser.value ?: return@CIFileView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import chat.simplex.common.views.helpers.fontSizeMultiplier
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.common.views.onboarding.GetStakeView
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// Spec: spec/client/chat-list.md#GetStakeBanner
|
||||
@Composable
|
||||
fun GetStakeBanner(showDismiss: Boolean, onTap: () -> Unit, onDismiss: () -> Unit) {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.bannerCard(onTap)
|
||||
// the logo sits roughly where the phone of the badge banner's hero is, 40dp from the card edge
|
||||
.padding(start = 16.dp, end = 40.dp, top = 12.dp, bottom = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
stringResource(MR.strings.invest_banner_title),
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.invest_banner_subtitle),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Image(painterResource(MR.images.wefunder_logo), contentDescription = null, modifier = Modifier.size(48.dp))
|
||||
}
|
||||
|
||||
if (showDismiss) {
|
||||
BannerDismissButton(Modifier.align(Alignment.TopEnd), onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGetStake(modalManager: ModalManager) {
|
||||
appPrefs.getStakeBannerTapped.set(true)
|
||||
modalManager.showModalCloseable(cardScreen = true) { close ->
|
||||
GetStakeView(showFirstImage = true, inCenterOfWindow = modalManager === ModalManager.center, close = close)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.bannerCard(onTap: () -> Unit): Modifier {
|
||||
// grows linearly with system font but never shrinks below the default, and the card grows further
|
||||
// when 2-line text wraps at very large fonts
|
||||
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
|
||||
val isDark = isInDarkTheme()
|
||||
var cardSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
|
||||
return this
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cardHeight)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(brush)
|
||||
.clickable(onClick = onTap)
|
||||
.onSizeChanged { cardSize = it }
|
||||
}
|
||||
|
||||
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
|
||||
@Composable
|
||||
fun BannerDismissButton(modifier: Modifier, onDismiss: () -> Unit) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_close_button),
|
||||
tint = if (isInDarkTheme()) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||
modifier = modifier
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onDismiss)
|
||||
.padding(8.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Geometry-aware gradient with asymmetric scale: start (dark) pushed further below the card than
|
||||
// end (warm) is above, so card-middle lands at the bright/mid-transition stop, not the dark region.
|
||||
private fun gradientBrush(isDark: Boolean, size: IntSize): Brush {
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
if (size.width == 0 || size.height == 0) return Brush.linearGradient(colorStops = stops)
|
||||
val w = size.width.toFloat()
|
||||
val h = size.height.toFloat()
|
||||
val startScale = if (isDark) 3.0f else 2.5f
|
||||
val endScale = if (isDark) 2.1f else 1.7f
|
||||
val gp = gradientPoints(h / w, 1.0f)
|
||||
val sx = 0.5f + (gp.startX - 0.5f) * startScale
|
||||
val sy = 0.5f + (gp.startY - 0.5f) * startScale
|
||||
val ex = 0.5f + (gp.endX - 0.5f) * endScale
|
||||
val ey = 0.5f + (gp.endY - 0.5f) * endScale
|
||||
return Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(sx * w, sy * h),
|
||||
end = Offset(ex * w, ey * h)
|
||||
)
|
||||
}
|
||||
+10
-2
@@ -207,9 +207,17 @@ private fun BadgeGlyph(badge: LocalBadge, modifier: Modifier, onBadgeClick: (()
|
||||
}
|
||||
}
|
||||
|
||||
// verbatim for an unknown type: it is the service's string, and must not be looked up as a localised key
|
||||
fun badgeTypeName(t: BadgeType): String = when (t) {
|
||||
is BadgeType.Supporter -> generalGetString(MR.strings.badges_level_supporter)
|
||||
is BadgeType.Legend -> generalGetString(MR.strings.badges_level_legend)
|
||||
is BadgeType.Investor -> generalGetString(MR.strings.badges_type_investor)
|
||||
is BadgeType.Unknown -> t.type
|
||||
}
|
||||
|
||||
fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler) {
|
||||
// a verified badge's type is signed and can't be faked, so the real (possibly unknown) type name is the title
|
||||
val title = badge.badge.badgeType.text.replaceFirstChar { it.uppercase() }
|
||||
val title = badgeTypeName(badge.badge.badgeType)
|
||||
when {
|
||||
badge.status == BadgeStatus.Failed ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -244,7 +252,7 @@ fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private fun badgeImage(t: BadgeType): ImageResource = when (t) {
|
||||
fun badgeImage(t: BadgeType): ImageResource = when (t) {
|
||||
is BadgeType.Legend -> MR.images.badge_legend
|
||||
is BadgeType.Investor -> MR.images.badge_investor
|
||||
else -> MR.images.badge_supporter // Supporter + Unknown
|
||||
|
||||
+39
-12
@@ -18,6 +18,7 @@ import com.charleskorn.kaml.decodeFromStream
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
@@ -27,6 +28,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.*
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
@@ -123,10 +126,13 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000
|
||||
|
||||
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
|
||||
|
||||
// raised XFTP receive limits for files from a sender with a supporter badge (also investor) or a legend badge
|
||||
// raised XFTP limits for a user with a supporter badge (also investor) or a legend badge
|
||||
const val MAX_FILE_SIZE_XFTP_SUPPORTER: Long = 2_147_483_648 // 2GB
|
||||
const val MAX_FILE_SIZE_XFTP_LEGEND: Long = 5_368_709_120 // 5GB
|
||||
|
||||
// a badge raises the limit at send for this long after its expiry, shorter than the receiver's grace
|
||||
val BADGE_SND_GRACE_INTERVAL: Duration = 1.days
|
||||
|
||||
const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE
|
||||
|
||||
expect fun getAppFileUri(fileName: String): URI
|
||||
@@ -474,25 +480,46 @@ fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in
|
||||
return fileCount to bytes
|
||||
}
|
||||
|
||||
fun badgeMaxFileSize(badge: LocalBadge): Long =
|
||||
if (badge.badge.badgeType == BadgeType.Legend) MAX_FILE_SIZE_XFTP_LEGEND else MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
|
||||
// a badge raises the limit at send until one day past its expiry, as the core applies it
|
||||
fun badgeActiveForSend(badge: LocalBadge): Boolean =
|
||||
badge.status == BadgeStatus.Active && badge.badge.badgeExpiry + BADGE_SND_GRACE_INTERVAL >= Clock.System.now()
|
||||
|
||||
// in incognito chats and above the largest badge's limit no badge applies, so badgeIssue is not used
|
||||
fun largeFileMessage(fileSize: Long, incognito: Boolean = false, badgeIssue: String = ""): String =
|
||||
if (incognito) {
|
||||
String.format(generalGetString(MR.strings.large_file_incognito), formatBytes(MAX_FILE_SIZE_XFTP))
|
||||
} else if (fileSize > MAX_FILE_SIZE_XFTP_LEGEND) {
|
||||
String.format(generalGetString(MR.strings.max_file_size_with_badge), formatBytes(MAX_FILE_SIZE_XFTP_LEGEND), generalGetString(MR.strings.legend_badge))
|
||||
} else {
|
||||
val supporter = fileSize <= MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
val message = String.format(
|
||||
generalGetString(MR.strings.large_file_requires_badge),
|
||||
generalGetString(if (supporter) MR.strings.supporter_badge else MR.strings.legend_badge),
|
||||
formatBytes(if (supporter) MAX_FILE_SIZE_XFTP else MAX_FILE_SIZE_XFTP_SUPPORTER)
|
||||
)
|
||||
if (badgeIssue.isEmpty()) message else message + " " + badgeIssue
|
||||
}
|
||||
|
||||
// the badge lapsed, and while active it would have allowed this file
|
||||
fun expiredBadgeReason(fileSize: Long, senderProfile: LocalProfile?): String {
|
||||
val badge = senderProfile?.localBadge
|
||||
return if (badge != null && !badgeActiveForSend(badge) && badgeMaxFileSize(badge) >= fileSize) {
|
||||
generalGetString(MR.strings.your_badge_expired)
|
||||
} else ""
|
||||
}
|
||||
|
||||
fun getMaxFileSize(fileProtocol: FileProtocol, senderProfile: LocalProfile? = null): Long = when (fileProtocol) {
|
||||
FileProtocol.SMP -> MAX_FILE_SIZE_SMP
|
||||
FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL
|
||||
// a sender's active badge raises the XFTP limit: legend to 5GB, any other (supporter/investor) to 2GB
|
||||
FileProtocol.XFTP -> {
|
||||
val badge = senderProfile?.localBadge
|
||||
if (badge == null || badge.status != BadgeStatus.Active) MAX_FILE_SIZE_XFTP
|
||||
else if (badge.badge.badgeType == BadgeType.Legend) MAX_FILE_SIZE_XFTP_LEGEND
|
||||
else MAX_FILE_SIZE_XFTP_SUPPORTER
|
||||
if (badge != null && badgeActiveForSend(badge)) badgeMaxFileSize(badge) else MAX_FILE_SIZE_XFTP
|
||||
}
|
||||
}
|
||||
|
||||
// the profile of whoever sent a received chat item - the group member, or the direct chat's contact
|
||||
fun ciSenderProfile(ci: ChatItem, chatInfo: ChatInfo): LocalProfile? = when (val dir = ci.chatDir) {
|
||||
is CIDirection.GroupRcv -> dir.groupMember.memberProfile
|
||||
is CIDirection.DirectRcv -> (chatInfo as? ChatInfo.Direct)?.contact?.profile
|
||||
else -> null
|
||||
}
|
||||
|
||||
expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration
|
||||
|
||||
// Whether the file really contains a video track. Reads container metadata only, without decoding a frame.
|
||||
|
||||
+4
-4
@@ -64,7 +64,7 @@ fun PasscodeView(
|
||||
@Composable
|
||||
fun VerticalLayout() {
|
||||
Column(
|
||||
Modifier.handleKeyboard().focusRequester(focusRequester),
|
||||
Modifier.systemBarsPadding().handleKeyboard().focusRequester(focusRequester),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
@@ -74,7 +74,7 @@ fun PasscodeView(
|
||||
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, true)
|
||||
PasscodeEntry(passcode, true, Modifier.weight(1f, fill = false))
|
||||
Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
@@ -85,9 +85,9 @@ fun PasscodeView(
|
||||
|
||||
@Composable
|
||||
fun HorizontalLayout() {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) {
|
||||
Row(Modifier.systemBarsPadding().padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) {
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING),
|
||||
Modifier.weight(1f, fill = false).padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
|
||||
+2
-1
@@ -22,8 +22,9 @@ import chat.simplex.res.MR
|
||||
fun PasscodeEntry(
|
||||
password: MutableState<String>,
|
||||
vertical: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PasscodeView(password)
|
||||
BoxWithConstraints {
|
||||
if (vertical) {
|
||||
|
||||
+36
-3
@@ -23,6 +23,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -32,7 +33,10 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.GetStakeBanner
|
||||
import chat.simplex.common.views.chatlist.openGetStake
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.crowdfundingAvailable
|
||||
import chat.simplex.common.views.usersettings.UserAddressView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -42,13 +46,21 @@ import kotlin.math.sin
|
||||
private const val CARD_HEIGHT_RATIO = 0.75f
|
||||
private const val GRADIENT_ANGLE_RAD = 80.0 * Math.PI / 180.0
|
||||
|
||||
// the onboarding cards replace the whole chat list, and the support-ended banner lives in the
|
||||
// list - a lapsed supporter is not a newcomer, and must be told even with no conversations yet
|
||||
@Composable
|
||||
fun shouldShowOnboarding(): Boolean {
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||
val chats = chatModel.chats.value
|
||||
return !addressCreationCardShown.value && chats.isNotEmpty() && !hasConversations(chats)
|
||||
return !addressCreationCardShown.value && chats.isNotEmpty() && !hasConversations(chats) && !supportEnded()
|
||||
}
|
||||
|
||||
fun supportEnded(): Boolean =
|
||||
BadgeModel.alert.value?.kind == BadgeAlertKind.SupportEnded && BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)
|
||||
|
||||
fun hasShownBadge(): Boolean =
|
||||
BadgeModel.badgeState.value?.shown == true && BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)
|
||||
|
||||
fun hasConversations(chats: List<Chat>): Boolean =
|
||||
chats.any { chat ->
|
||||
when (val c = chat.chatInfo) {
|
||||
@@ -407,6 +419,27 @@ fun ConnectOnboardingView() {
|
||||
}
|
||||
}
|
||||
|
||||
val getStakeBannerDismissed = remember { appPrefs.getStakeBannerDismissed.state }
|
||||
val showGetStakeBanner = crowdfundingAvailable() && !getStakeBannerDismissed.value
|
||||
// on desktop the pages span the window, but the banner keeps the width it has in the chat list
|
||||
val bannerMaxWidth = if (appPlatform.isDesktop) DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier else Dp.Unspecified
|
||||
val content = @Composable {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
pager()
|
||||
}
|
||||
if (showGetStakeBanner) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).widthIn(max = bannerMaxWidth).padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 8.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = false,
|
||||
onTap = cardClickOverride ?: { openGetStake(if (appPlatform.isDesktop) ModalManager.center else ModalManager.start) },
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
val maxContentWidth = DEFAULT_WINDOW_WIDTH - DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier
|
||||
Box(
|
||||
@@ -414,12 +447,12 @@ fun ConnectOnboardingView() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -7,6 +7,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
@@ -22,12 +23,12 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null, titleColor: Color = Color.Unspecified) {
|
||||
Column(Modifier.fillMaxSize().padding(horizontal = if (appPlatform.isDesktop) DEFAULT_PADDING * 2 else DEFAULT_PADDING)) {
|
||||
Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
val paraPadding = PaddingValues(bottom = if (appPlatform.isDesktop) 10.dp else 12.dp)
|
||||
Column(Modifier.weight(1f).padding(bottom = DEFAULT_PADDING).verticalScroll(rememberScrollState())) {
|
||||
Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, color = titleColor, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.why_built_p1, padding = paraPadding)
|
||||
ReadableText(MR.strings.why_built_p2, padding = paraPadding)
|
||||
ReadableText(MR.strings.why_built_p3, padding = paraPadding)
|
||||
@@ -45,6 +46,11 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
|
||||
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() })
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
} else {
|
||||
// No button below — add breathing room at the bottom on both platforms.
|
||||
// Android also gets nav-bar inset so content doesn't run under the gesture bar.
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
if (appPlatform.isAndroid) Spacer(Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -190,12 +190,14 @@ expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference
|
||||
fun OnboardingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
labelId: StringResource,
|
||||
labelArg: String? = null,
|
||||
onboarding: OnboardingStage?,
|
||||
enabled: Boolean = true,
|
||||
icon: Painter? = null,
|
||||
iconColor: Color = Color.White,
|
||||
onclick: (() -> Unit)?
|
||||
) {
|
||||
val label = if (labelArg != null) stringResource(labelId).format(labelArg) else stringResource(labelId)
|
||||
Button(
|
||||
onClick = {
|
||||
onclick?.invoke()
|
||||
@@ -211,9 +213,9 @@ fun OnboardingActionButton(
|
||||
colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary)
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor)
|
||||
Icon(icon, label, Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor)
|
||||
}
|
||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
|
||||
Text(label, style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+52
-4
@@ -969,8 +969,56 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
),
|
||||
)
|
||||
),
|
||||
// TODO [badges] restore when badges ship, with final copy and the Read more link.
|
||||
/*
|
||||
VersionDescription(
|
||||
version = "v7.1",
|
||||
post = null,
|
||||
features = listOf(
|
||||
VersionFeature.FeatureView(
|
||||
icon = null,
|
||||
titleId = MR.strings.v7_1_supporter_badge_title,
|
||||
view = { SupporterBadgeWhatsNew() }
|
||||
)
|
||||
)
|
||||
),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun SupporterBadgeWhatsNew() {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
stringResource(MR.strings.v7_1_supporter_badge_title),
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.v7_1_supporter_badge_body),
|
||||
style = MaterialTheme.typography.body1,
|
||||
maxLines = 10
|
||||
)
|
||||
}
|
||||
val isDark = isInDarkTheme()
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(110.dp)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
fun setLastVersionDefault(m: ChatModel) {
|
||||
@@ -1040,7 +1088,7 @@ fun isInUs(): Boolean =
|
||||
@Composable
|
||||
private fun InvestInSimpleXChatView(modalManager: ModalManager) {
|
||||
if (!crowdfundingAvailable()) return
|
||||
val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } }
|
||||
val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(showFirstImage = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } }
|
||||
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||
Text(
|
||||
generalGetString(MR.strings.v7_0_invest),
|
||||
@@ -1122,7 +1170,7 @@ private val getStakeSlides: List<CrowdfundingSlide> = listOf(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) {
|
||||
fun GetStakeView(showFirstImage: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@@ -1152,7 +1200,7 @@ fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close
|
||||
val title = "Get a stake in\nSimpleX Chat"
|
||||
AppBarTitle(if (inCenterOfWindow) title.replace("\n", " ") else title, withPadding = false)
|
||||
// What's new already shows the image of the first slide, above the link that opens this page
|
||||
if (fromSettings) {
|
||||
if (showFirstImage) {
|
||||
slideImage(getStakeSlides[0])
|
||||
}
|
||||
Text(
|
||||
@@ -1165,7 +1213,7 @@ fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.padding(top = if (fromSettings) 8.dp else 0.dp),
|
||||
Modifier.padding(top = if (showFirstImage) 8.dp else 0.dp),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
|
||||
+14
-1
@@ -26,6 +26,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.badges.BadgesView
|
||||
import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||
@@ -103,6 +104,18 @@ fun SettingsLayout(
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
// Direct showModal (no settings / cardScreen flags) — settings-style card chrome would render
|
||||
// a gray top bar / back button that badges views don't want (they have their own inline titles).
|
||||
SectionItemView(click = { ModalManager.start.showCustomModal { close -> BadgesView(close) } }) {
|
||||
val badgeType = chatModel.currentUser.value?.profile?.localBadge?.badge?.badgeType ?: BadgeType.Supporter
|
||||
Image(painterResource(badgeImage(badgeType)), stringResource(MR.strings.supporter_perks), Modifier.size(24.dp))
|
||||
TextIconSpaced()
|
||||
Text(stringResource(MR.strings.supporter_perks))
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.advanced_settings)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
|
||||
if (appPlatform == AppPlatform.ANDROID) {
|
||||
@@ -119,7 +132,7 @@ fun SettingsLayout(
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_redeem),
|
||||
stringResource(MR.strings.v7_0_crowdfunding),
|
||||
{ ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = true, close = close) } }
|
||||
{ ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(showFirstImage = true, close = close) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,15 @@
|
||||
<string name="accept_call_on_lock_screen">اقبل</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف تعريفك وجهات اتصالك ورسائلك وملفاتك بشكل نهائي.</string>
|
||||
<string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string>
|
||||
<string name="this_QR_code_is_not_a_link">رمز QR هذا ليس رابطًا!</string>
|
||||
<string name="next_generation_of_private_messaging">مستقبل المُراسلة</string>
|
||||
<string name="delete_files_and_media_desc">لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة.</string>
|
||||
<string name="enable_automatic_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق.</string>
|
||||
<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="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
|
||||
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string>
|
||||
<string name="allow_verb">اسمح</string>
|
||||
<string name="smp_servers_preset_add">أضِف خوادم مُعدة مسبقًا</string>
|
||||
<string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string>
|
||||
<string name="users_delete_all_chats_deleted">ستُحذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر وسيط SOCKS على المنفذ %d؟ يجب بدء تشغيل الوسيط قبل تفعيل هذا الخيار.</string>
|
||||
@@ -108,8 +103,6 @@
|
||||
<string name="icon_descr_call_ended">انتهت المكالمة</string>
|
||||
<string name="change_verb">غيِّر</string>
|
||||
<string name="color_primary_variant">لون إضافي ثانوي</string>
|
||||
<string name="available_in_v51">"
|
||||
\nمتوفر في v5.1"</string>
|
||||
<string name="calls_prohibited_with_this_contact">مكالمات الصوت/الفيديو ممنوعة.</string>
|
||||
<string name="v4_5_transport_isolation_descr">عن طريق ملف تعريف الدردشة (افتراضي) أو عن طريق الاتصال (تجريبي).</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> يمكن تعطيله عبر الإعدادات</b> - سيستمر عرض الإشعارات أثناء تشغيل التطبيق.]]></string>
|
||||
@@ -167,7 +160,6 @@
|
||||
<string name="error_deleting_contact">خطأ في حذف جهة الاتصال</string>
|
||||
<string name="notification_preview_somebody">جهة الاتصال مخفية:</string>
|
||||
<string name="copy_verb">انسخ</string>
|
||||
<string name="connect_via_link_verb">اتصل</string>
|
||||
<string name="server_connected">متصل</string>
|
||||
<string name="connect_via_group_link">انضمام إلى المجموعة؟</string>
|
||||
<string name="connect_via_invitation_link">اتصل عبر رابط لمرة واحدة؟</string>
|
||||
@@ -305,15 +297,11 @@
|
||||
<string name="icon_descr_server_status_connected">متصل</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="maximum_supported_file_size">الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s.</string>
|
||||
<string name="connect_via_link_or_qr">تواصل عبر الرابط / رمز QR</string>
|
||||
<string name="share_one_time_link">أنشئ رابط دعوة لمرة واحدة</string>
|
||||
<string name="smp_servers_check_address">تحقق من عنوان الخادم وحاول مرة أخرى.</string>
|
||||
<string name="clear_verification">امحُ التحقُّق</string>
|
||||
<string name="create_address_and_let_people_connect">أنشئ عنوانًا للسماح للأشخاص بالتواصل معك.</string>
|
||||
<string name="smp_servers_enter_manually">أدخل الخادم يدويًا</string>
|
||||
<string name="colored_text">ملون</string>
|
||||
<string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطرفين</string>
|
||||
<string name="create_profile_button">أنشئ</string>
|
||||
<string name="create_your_profile">أنشئ ملف تعريفك</string>
|
||||
<string name="icon_descr_call_connecting">مكالمة جارية</string>
|
||||
<string name="enable_self_destruct">فعّل التدمير الذاتي</string>
|
||||
@@ -387,7 +375,6 @@
|
||||
<string name="delete_contact_question">حذف جهة الاتصال؟</string>
|
||||
<string name="for_me_only">احذف بالنسبة لي</string>
|
||||
<string name="send_disappearing_message_custom_time">وقت مخصّص</string>
|
||||
<string name="decentralized">لامركزي</string>
|
||||
<string name="database_passphrase">عبارة مرور قاعدة البيانات</string>
|
||||
<string name="current_passphrase">عبارة المرور الحالية…</string>
|
||||
<string name="database_encryption_will_be_updated">سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في Keystore.</string>
|
||||
@@ -483,7 +470,6 @@
|
||||
<string name="custom_time_unit_hours">ساعات</string>
|
||||
<string name="edit_history">السجل</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز QR في مكالمة الفيديو</b>، أو شارك الرابط.]]></string>
|
||||
<string name="install_simplex_chat_for_terminal">ثبّت SimpleX Chat لطرفية</string>
|
||||
<string name="network_disable_socks_info">إذا أكّدت، فستتمكن خوادم المُراسلة من رؤية عنوان IP الخاص بك ومزود خدمتك - أي الخوادم التي تتصل بها.</string>
|
||||
<string name="hide_dev_options">إخفاء:</string>
|
||||
@@ -505,8 +491,6 @@
|
||||
<string name="info_menu">معلومات</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">تحسن الخصوصية والأمان</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]></string>
|
||||
<string name="immune_to_spam_and_abuse">محصن ضد الإزعاج (spam)</string>
|
||||
<string name="description_via_one_time_link_incognito">التخفي عبر رابط لمرة واحدة</string>
|
||||
<string name="icon_descr_image_snd_complete">أرسلت صورة</string>
|
||||
<string name="image_descr">صورة</string>
|
||||
@@ -547,7 +531,6 @@
|
||||
<string name="v5_1_japanese_portuguese_interface">واجهة أستخدام يابانية وبرتغالية</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النُسخة الاحتياطية القديمة لقاعدة البيانات.</string>
|
||||
<string name="group_preview_join_as">انضم ك%s</string>
|
||||
<string name="invalid_QR_code">رمز QR غير صالح</string>
|
||||
<string name="v4_5_italian_interface">الواجهة الإيطالية</string>
|
||||
<string name="invalid_contact_link">الرابط غير صالح!</string>
|
||||
<string name="invite_friends">دعوة الأصدقاء</string>
|
||||
@@ -647,18 +630,12 @@
|
||||
<string name="ttl_sec">%d ثانية</string>
|
||||
<string name="icon_descr_server_status_disconnected">قُطع الاتصال</string>
|
||||
<string name="disappearing_message">رسالة تختفي</string>
|
||||
<string name="dont_create_address">لا تنشئ عنوانًا</string>
|
||||
<string name="error_setting_network_config">خطأ في تحديث تضبيط الشبكة</string>
|
||||
<string name="error_receiving_file">خطأ في استلام الملف</string>
|
||||
<string name="failed_to_active_user_title">خطأ في تبديل ملف التعريف!</string>
|
||||
<string name="v5_2_fix_encryption">حافظ على اتصالاتك</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم XFTP بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string>
|
||||
<string name="marked_deleted_description">عُلّم محذوف</string>
|
||||
<string name="color_surface">القوائم والتنبيهات</string>
|
||||
<string name="error_saving_xftp_servers">خطأ في حفظ خوادم XFTP</string>
|
||||
<string name="error_loading_smp_servers">خطأ في تحميل خوادم SMP</string>
|
||||
<string name="error_loading_xftp_servers">خطأ في تحميل خوادم XFTP</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم SMP في التنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string>
|
||||
<string name="markdown_help">مساعدة ماركداون</string>
|
||||
<string name="group_member_role_member">عضو</string>
|
||||
<string name="markdown_in_messages">ماركداون في الرسائل</string>
|
||||
@@ -690,7 +667,6 @@
|
||||
<string name="v5_2_fix_encryption_descr">إصلاح التعمية بعد استعادة النُسخ الاحتياطية.</string>
|
||||
<string name="v5_2_disappear_one_message">اجعل رسالة واحدة تختفي</string>
|
||||
<string name="error_enabling_delivery_receipts">خطأ في تفعيل إيصالات التسليم!</string>
|
||||
<string name="error_saving_smp_servers">خطأ في حفظ خوادم SMP</string>
|
||||
<string name="error_sending_message">خطأ في إرسال الرسالة</string>
|
||||
<string name="error_joining_group">خطأ في الانضمام إلى المجموعة</string>
|
||||
<string name="error_synchronizing_connection">خطأ في مزامنة الاتصال</string>
|
||||
@@ -777,7 +753,6 @@
|
||||
<string name="network_use_onion_hosts_required_desc">ستكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string>
|
||||
<string name="self_destruct_new_display_name">اسم عرض جديد:</string>
|
||||
<string name="new_passphrase">عبارة مرور جديدة…</string>
|
||||
<string name="icon_descr_server_status_pending">قيد الانتظار</string>
|
||||
<string name="enter_passphrase_notification_title">عبارة المرور مطلوبة</string>
|
||||
<string name="paste_the_link_you_received">ألصِق الرابط الذي استلمته</string>
|
||||
<string name="only_owners_can_enable_files_and_media">فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط.</string>
|
||||
@@ -808,12 +783,9 @@
|
||||
<string name="auth_open_chat_console">افتح وحدة تحكم الدردشة</string>
|
||||
<string name="la_lock_mode_passcode">إدخال رمز المرور</string>
|
||||
<string name="open_simplex_chat_to_accept_call">افتح SimpleX Chat لقبول المكالمة</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">يمكن لأي شخص استضافة الخوادم.</string>
|
||||
<string name="password_to_show">كلمة المرور للإظهار</string>
|
||||
<string name="call_connection_peer_to_peer">ندّ لِندّ</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">أنت تقرر مَن يمكنه الاتصال.</string>
|
||||
<string name="icon_descr_call_pending_sent">مكالمة قيد الانتظار</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل.</string>
|
||||
<string name="reset_color">صفّر الألوان</string>
|
||||
<string name="save_verb">احفظ</string>
|
||||
<string name="smp_servers_preset_address">عنوان الخادم المُعد مسبقًا</string>
|
||||
@@ -836,7 +808,6 @@
|
||||
<string name="rate_the_app">قيم التطبيق</string>
|
||||
<string name="port_verb">منفذ</string>
|
||||
<string name="save_auto_accept_settings">احفظ إعدادات عنوان SimpleX</string>
|
||||
<string name="privacy_redefined">إعادة تعريف الخصوصية</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">يُرجى إبلاغ المطوِّرين بذلك.</string>
|
||||
<string name="privacy_and_security">الخصوصية والأمان</string>
|
||||
<string name="remove_passphrase">أزل</string>
|
||||
@@ -850,7 +821,6 @@
|
||||
<string name="saved_ICE_servers_will_be_removed">ستتم إزالة خوادم WebRTC ICE المحفوظة.</string>
|
||||
<string name="prohibit_sending_files">امنع إرسال الملفات والوسائط.</string>
|
||||
<string name="callstate_received_answer">استلمت إجابة…</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">مستودع GitHub</font>.]]></string>
|
||||
<string name="reject">ارفض</string>
|
||||
<string name="relay_server_protects_ip">يحمي خادم المُرحل عنوان IP الخاص بك، ولكن يمكنه مراقبة مُدّة المكالمة.</string>
|
||||
<string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string>
|
||||
@@ -922,7 +892,6 @@
|
||||
<string name="image_descr_qr_code">رمز QR</string>
|
||||
<string name="reset_verb">صفّر</string>
|
||||
<string name="network_proxy_port">المنفذ %d</string>
|
||||
<string name="smp_servers_preset_server">خادم مُعد مسبقًا</string>
|
||||
<string name="relay_server_if_necessary">يُستخدم خادم المُرحل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك.</string>
|
||||
<string name="save_and_notify_contact">حفظ وإشعار جهة الاتصال</string>
|
||||
<string name="settings_restart_app">إعادة التشغيل</string>
|
||||
@@ -934,7 +903,6 @@
|
||||
<string name="v5_1_self_destruct_passcode">رمز المرور للتدمير الذاتي</string>
|
||||
<string name="sending_files_not_yet_supported">إرسال الملفات غير مدعوم بعد</string>
|
||||
<string name="sender_cancelled_file_transfer">أُلغيَ المرسل نقل الملف.</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(امسح أو ألصق من الحافظة)</string>
|
||||
<string name="network_option_seconds_label">ثانية</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">حذف المُرسل طلب الاتصال.</string>
|
||||
<string name="scan_QR_code">امسح رمز QR</string>
|
||||
@@ -1007,7 +975,6 @@
|
||||
<string name="la_notice_title_simplex_lock">قفل SimpleX</string>
|
||||
<string name="is_not_verified">لم يتحقق من %s</string>
|
||||
<string name="chat_lock">قفل SimpleX</string>
|
||||
<string name="smp_servers">خوادم SMP</string>
|
||||
<string name="share_image">مشاركة الوسائط…</string>
|
||||
<string name="ntf_channel_messages">رسائل SimpleX Chat</string>
|
||||
<string name="lock_not_enabled">قفل SimpleX غير مفعّل!</string>
|
||||
@@ -1024,7 +991,6 @@
|
||||
<string name="stop_chat_question">إيقاف الدردشة؟</string>
|
||||
<string name="show_call_on_lock_screen">أظهر</string>
|
||||
<string name="non_fatal_errors_occured_during_import">حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد:</string>
|
||||
<string name="settings_section_title_socks">وسيط SOCKS</string>
|
||||
<string name="v4_2_security_assessment_desc">تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits.</string>
|
||||
<string name="stop_chat_confirmation">إيقاف</string>
|
||||
<string name="settings_notification_preview_mode_title">أظهر المعاينة</string>
|
||||
@@ -1077,20 +1043,15 @@
|
||||
<string name="periodic_notifications_desc">يجلب التطبيق الرسائل الجديدة بشكل دوري - يستخدم نسبة قليلة من البطارية يوميًا. لا يستخدم التطبيق إشعارات الدفع - لا يتم إرسال البيانات من جهازك إلى الخوادم.</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">سيتم إلغاء الاتصال الذي قبلته!</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال!</string>
|
||||
<string name="this_text_is_available_in_settings">هذا النص متاح في الإعدادات</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك.</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة.</string>
|
||||
<string name="network_session_mode_transport_isolation">عزل النقل</string>
|
||||
<string name="v4_4_french_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string>
|
||||
<string name="v4_6_audio_video_calls_descr">دعم البلوتوث وتحسينات أخرى.</string>
|
||||
<string name="v5_0_polish_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[لتحسين الخصوصية، <b>يتم تشغيل SimpleX في الخلفية</b> بدلاً من استخدام إشعارات push.]]></string>
|
||||
<string name="tap_to_start_new_chat">انقر لبدء محادثة جديدة</string>
|
||||
<string name="to_share_with_your_contact">(للمشاركة مع جهة اتصالك)</string>
|
||||
<string name="to_connect_via_link_title">للتواصل عبر الرابط</string>
|
||||
<string name="scan_qr_to_connect_to_contact">للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق.</string>
|
||||
<string name="smp_servers_test_servers">اختبر الخوادم</string>
|
||||
<string name="first_platform_without_user_ids">لا معرّفات مُستخدم</string>
|
||||
<string name="settings_section_title_support">دعم SimpleX Chat</string>
|
||||
<string name="switch_verb">بدِّل</string>
|
||||
<string name="color_title">العنوان الرئيسي</string>
|
||||
@@ -1143,7 +1104,6 @@
|
||||
<string name="you_will_join_group">ستتصل بجميع أعضاء المجموعة.</string>
|
||||
<string name="your_chat_profiles">ملفات تعريف دردشتك</string>
|
||||
<string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string>
|
||||
<string name="your_SMP_servers">خوادم SMP الخاصة بك</string>
|
||||
<string name="onboarding_notifications_mode_off">عندما يكون التطبيق قيد التشغيل</string>
|
||||
<string name="call_connection_via_relay">عبر المُرحل</string>
|
||||
<string name="you_joined_this_group">لقد انضممت إلى هذه المجموعة</string>
|
||||
@@ -1155,7 +1115,6 @@
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال رفع جهة اتصالك.</string>
|
||||
<string name="verify_security_code">تحقق من رمز الأمان</string>
|
||||
<string name="v4_3_voice_messages">رسائل صوتية</string>
|
||||
<string name="you_can_accept_or_reject_connection">عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه.</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا!</string>
|
||||
<string name="using_simplex_chat_servers">تستخدم خوادم SimpleX Chat.</string>
|
||||
@@ -1164,9 +1123,7 @@
|
||||
<string name="network_enable_socks">استخدام وسيط SOCKS؟</string>
|
||||
<string name="network_use_onion_hosts_prefer">عندما تكون متاحة</string>
|
||||
<string name="your_contacts_will_remain_connected">ستبقى جهات اتصالك متصلة.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم.</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">يمكنك استخدام تخفيض السعر لتنسيق الرسائل:</string>
|
||||
<string name="use_chat">استخدم الدردشة</string>
|
||||
<string name="settings_section_title_you">أنت</string>
|
||||
<string name="unknown_error">خطأ غير معروف</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">غيّرتَ دور نفسك إلى %s</string>
|
||||
@@ -1182,12 +1139,9 @@
|
||||
<string name="waiting_for_file">في انتظار الملف</string>
|
||||
<string name="view_security_code">عرض رمز الأمان</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">لن تفقد جهات اتصالك إذا حذفت عنوانك لاحقًا.</string>
|
||||
<string name="your_XFTP_servers">خوادم XFTP الخاصة بك</string>
|
||||
<string name="use_simplex_chat_servers__question">استخدم خوادم SimpleX Chat</string>
|
||||
<string name="xftp_servers">خوادم XFTP</string>
|
||||
<string name="your_ICE_servers">خوادم ICE الخاصة بك</string>
|
||||
<string name="network_disable_socks">هل تستخدم اتصالاً مباشرًا بالإنترنت؟</string>
|
||||
<string name="you_control_your_chat">أنت تتحكم في الدردشة!</string>
|
||||
<string name="your_calls">مكالماتك</string>
|
||||
<string name="wrong_passphrase_title">عبارة مرور خاطئة!</string>
|
||||
<string name="database_downgrade_warning">تحذير: قد تفقد بعض البيانات!</string>
|
||||
@@ -1211,7 +1165,6 @@
|
||||
<string name="group_main_profile_sent">سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة</string>
|
||||
<string name="personal_welcome">مرحبًا! %1$s</string>
|
||||
<string name="contact_wants_to_connect_with_you">يريد الاتصال بك!</string>
|
||||
<string name="your_ice_servers">خوادم ICE الخاصة بك</string>
|
||||
<string name="your_privacy">خصوصيتك</string>
|
||||
<string name="rcv_group_event_updated_group_profile">حُدثت ملف تعريف المجموعة</string>
|
||||
<string name="group_info_member_you">أنت: %1$s</string>
|
||||
@@ -1233,7 +1186,6 @@
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">سيُطلب منك المصادقة عند بدء تشغيل التطبيق أو استئنافه بعد 30 ثانية في الخلفية.</string>
|
||||
<string name="voice_message_with_duration">رسالة صوتية (%1$s)</string>
|
||||
<string name="your_settings">إعداداتك</string>
|
||||
<string name="update_network_session_mode_question">تحديث وضع عزل النقل؟</string>
|
||||
<string name="your_profile_is_stored_on_your_device">يُخزن ملف التعريف وجهات الاتصال والرسائل التي سلُمت على جهازك.</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة.
|
||||
\nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string>
|
||||
@@ -1245,13 +1197,10 @@
|
||||
<string name="your_current_profile">ملف تعريفك الحالي</string>
|
||||
<string name="simplex_link_connection">عبر %1$s</string>
|
||||
<string name="icon_descr_received_msg_status_unread">غير مقروءة</string>
|
||||
<string name="welcome">مرحبًا!</string>
|
||||
<string name="icon_descr_waiting_for_image">في انتظار الصورة</string>
|
||||
<string name="video_descr">فيديو</string>
|
||||
<string name="icon_descr_waiting_for_video">في انتظار الفيديو</string>
|
||||
<string name="gallery_video_button">فيديو</string>
|
||||
<string name="you_can_share_your_address">يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك.</string>
|
||||
<string name="you_can_create_it_later">يمكنك إنشاؤه لاحقًا</string>
|
||||
<string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال شاركت ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملف تعريفك الرئيسي</string>
|
||||
<string name="user_unmute">ألغِ الكتم</string>
|
||||
<string name="unmute_chat">ألغِ الكتم</string>
|
||||
@@ -1259,7 +1208,6 @@
|
||||
<string name="unhide_profile">إلغاء إخفاء ملف تعريف</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال.
|
||||
\nيمكنك إلغاء هذا الاتصال وإزالة جهة الاتصال (والمحاولة لاحقًا باستخدام رابط جديد).</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[يمكنك أيضًا الاتصال بالضغط على الرابط. إذا تم فتحه في المتصفح، فانقر فوق الزر <b>فتح في تطبيق الجوّال</b>.]]></string>
|
||||
<string name="smp_servers_use_server_for_new_conn">استخدم للاتصالات الجديدة</string>
|
||||
<string name="smp_servers_use_server">استخدم الخادم</string>
|
||||
<string name="smp_servers_your_server_address">عنوان خادمك</string>
|
||||
@@ -1281,7 +1229,6 @@
|
||||
<string name="button_welcome_message">رسالة الترحيب</string>
|
||||
<string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string>
|
||||
<string name="connection_error_auth_desc">أُزيلت جهة اتصالك هذا الرابط، أو أنه كان رابطًا لمرة واحدة وقد استُخدِم بالفعل.\nللتواصل، اطلب من جهة اتصالك إنشاء رابط جديد.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف دردشتك\nإلى جهة اتصالك</string>
|
||||
<string name="user_unhide">إلغاء الإخفاء</string>
|
||||
<string name="incognito_random_profile">ملفك التعريفي العشوائي</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة.</string>
|
||||
@@ -1298,7 +1245,6 @@
|
||||
<string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها.</string>
|
||||
<string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color="#0088ff">الاتصال بمطوِّري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string>
|
||||
<string name="smp_servers_your_server">خادمك</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك.</string>
|
||||
<string name="icon_descr_video_off">الفيديو مقفل</string>
|
||||
<string name="icon_descr_video_on">الفيديو مُشغَّل</string>
|
||||
@@ -1323,13 +1269,11 @@
|
||||
<string name="receipts_section_groups">مجموعات صغيرة (الحد الأقصى 20)</string>
|
||||
<string name="connect_via_member_address_alert_title">تواصل مباشرةً؟</string>
|
||||
<string name="connect_via_member_address_alert_desc">سيتم إرسال طلب الاتصال لعضو المجموعة هذا.</string>
|
||||
<string name="connect_via_link_incognito">اتصال متخفي</string>
|
||||
<string name="connect_use_current_profile">استخدم ملف التعريف الحالي</string>
|
||||
<string name="disable_notifications_button">عطّل الإشعارات</string>
|
||||
<string name="turn_off_system_restriction_button">افتح إعدادات التطبيق</string>
|
||||
<string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">ألصِق الرابط المُستلَم للتواصل مع جهة اتصالك…</string>
|
||||
<string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك التعريفي %1$s.</string>
|
||||
<string name="system_restricted_background_in_call_desc">قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.</string>
|
||||
<string name="turn_off_battery_optimization_button">اسمح</string>
|
||||
@@ -1498,7 +1442,6 @@
|
||||
<string name="error_showing_content">خطأ في إظهار المحتوى</string>
|
||||
<string name="error_showing_message">خطأ في إظهار الرسالة</string>
|
||||
<string name="callstatus_ended">انتهت المكالمة %1$s</string>
|
||||
<string name="you_can_make_address_visible_via_settings">يمكنك جعله مرئيًا لجهات اتصال SimpleX الخاصة بك عبر الإعدادات.</string>
|
||||
<string name="recent_history_is_not_sent_to_new_members">لا يتم إرسال التاريخ إلى الأعضاء الجدد.</string>
|
||||
<string name="retry_verb">حاول مجددًا</string>
|
||||
<string name="camera_not_available">الكاميرا غير متوفرة</string>
|
||||
@@ -1754,12 +1697,10 @@
|
||||
<string name="network_smp_proxy_mode_unprotected">غير محمي</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">السماح بالرجوع إلى إصدار سابق</string>
|
||||
<string name="network_smp_proxy_mode_never_description">لا تستخدم التوجيه الخاص.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">وضع توجيه الرسائل</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">لا</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">عندما يكون IP مخفيًا</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">لا ترسل رسائل مباشرةً، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">أرسل الرسائل مباشرة عندما لا يدعم الخادم الوجهة الخاص بك أو الخادم الوجهة التوجيه الخاص.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">احتياطي توجيه الرسالة</string>
|
||||
<string name="private_routing_show_message_status">أظهِر حالة الرسالة</string>
|
||||
<string name="protect_ip_address">احمِ عنوان IP</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لخوادم الملفات.</string>
|
||||
@@ -1775,7 +1716,6 @@
|
||||
<string name="file_not_approved_title">خوادم غير معروفة!</string>
|
||||
<string name="file_not_approved_descr">بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لمُرحلات XFTP هذه:
|
||||
\n%1$s.</string>
|
||||
<string name="chat_list_always_visible">أظهِر قائمة الدردشة في نافذة جديدة</string>
|
||||
<string name="settings_section_title_chat_colors">ألوان الدردشة</string>
|
||||
<string name="settings_section_title_chat_theme">سمة الدردشة</string>
|
||||
<string name="color_received_quote">تلقيت رد</string>
|
||||
@@ -1848,10 +1788,6 @@
|
||||
<string name="message_forwarded_title">رسالة مُحوّلة</string>
|
||||
<string name="message_forwarded_desc">لا يوجد اتصال مباشر حتى الآن، الرسالة مُحوّلة بواسطة المُدير.</string>
|
||||
<string name="scan_paste_link">ألصِق رابط / امسح</string>
|
||||
<string name="smp_servers_configured">خوادم SMP المهيأة</string>
|
||||
<string name="smp_servers_other">خوادم SMP أخرى</string>
|
||||
<string name="xftp_servers_configured">خوادم XFTP المهيأة</string>
|
||||
<string name="xftp_servers_other">خوادم XFTP أخرى</string>
|
||||
<string name="subscription_percentage">أظهِر النسبة المئوية</string>
|
||||
<string name="app_check_for_updates_disabled">مُعطَّل</string>
|
||||
<string name="app_check_for_updates_stable">مستقرّ</string>
|
||||
@@ -1897,7 +1833,6 @@
|
||||
<string name="downloaded_files">الملفات التي نُزّلت</string>
|
||||
<string name="download_errors">أخطاء التنزيل</string>
|
||||
<string name="expired_label">منتهية الصلاحيّة</string>
|
||||
<string name="open_server_settings_button">افتح إعدادات الخادم</string>
|
||||
<string name="other_label">أخرى</string>
|
||||
<string name="proxied">موّكل</string>
|
||||
<string name="secured">مؤمن</string>
|
||||
@@ -1962,9 +1897,7 @@
|
||||
<string name="privacy_media_blur_radius_medium">متوسط</string>
|
||||
<string name="privacy_media_blur_radius_soft">ناعم</string>
|
||||
<string name="info_view_call_button">مكالمة</string>
|
||||
<string name="info_view_connect_button">اتصل</string>
|
||||
<string name="info_view_message_button">مراسلة</string>
|
||||
<string name="info_view_open_button">افتح</string>
|
||||
<string name="info_view_search_button">بحث</string>
|
||||
<string name="toolbar_settings">الإعدادات</string>
|
||||
<string name="info_view_video_button">فيديو</string>
|
||||
@@ -2107,9 +2040,6 @@
|
||||
<string name="simplex_address_or_1_time_link">عنوان SimpleX أو رابط لمرة واحدة؟</string>
|
||||
<string name="onboarding_network_operators">مُشغلي الشبكة</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">يمكنك تضبيط الخوادم عبر الإعدادات.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">حدد مشغلي الشبكة الذين تريد استخدامهم.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">يمكنك تضبيط المُشغلين في إعدادات الشبكة والخوادم.</string>
|
||||
<string name="onboarding_network_operators_update">حدّث</string>
|
||||
<string name="onboarding_network_operators_continue">تابع</string>
|
||||
<string name="operator_conditions_accepted">الشروط المتفق عليها</string>
|
||||
<string name="operator_review_conditions">راجع الشروط</string>
|
||||
@@ -2121,7 +2051,6 @@
|
||||
<string name="operator_info_title">مُشغل الشبكة</string>
|
||||
<string name="operator">المُشغل</string>
|
||||
<string name="operator_servers_title">%s خوادم</string>
|
||||
<string name="operator_website">الموقع الإلكتروني</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">سيتم قبول الشروط في: %s.</string>
|
||||
<string name="operator_conditions_accepted_on">قُبل الشروط في: %s.</string>
|
||||
<string name="use_servers_of_operator_x">استخدم %s</string>
|
||||
@@ -2151,14 +2080,12 @@
|
||||
<string name="v6_2_network_decentralization_enable_flux">فعّل flux في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية.</string>
|
||||
<string name="v6_2_network_decentralization">اللامركزية الشبكية</string>
|
||||
<string name="v6_2_network_decentralization_descr">المُشغل المُعد مسبقًا الثاني في التطبيق!</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">لتحسين خصوصية البيانات الوصفية.</string>
|
||||
<string name="v6_2_improved_chat_navigation">تحسين التنقل في الدردشة</string>
|
||||
<string name="view_updated_conditions">اعرض الشروط المُحدثة</string>
|
||||
<string name="accept_conditions">اقبل الشروط</string>
|
||||
<string name="operator_added_message_servers">أُضيفت خوادم الرسائل</string>
|
||||
<string name="address_or_1_time_link">عنوان أو رابط لمرة واحدة؟</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[يمكن استخدام الرابط لمرة واحدة <i>مع جهة اتصال واحدة فقط</i> - المشاركة شخصيًا أو عبر أي مُراسل.]]></string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا.</string>
|
||||
<string name="onboarding_choose_server_operators">مُشغلي الخادم</string>
|
||||
<string name="operator_conditions_failed_to_load">لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط:</string>
|
||||
<string name="error_accepting_operator_conditions">خطأ في قبول الشروط</string>
|
||||
@@ -2171,12 +2098,10 @@
|
||||
<string name="no_message_servers_configured_for_receiving">لا يوجد خوادم لاستلام الرسائل.</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة.</string>
|
||||
<string name="you_can_set_connection_name_to_remember">يمكنك تعيين اسم الاتصال، لتتذكر الأشخاص الذين شاركتَ الرابط معهم.</string>
|
||||
<string name="onboarding_network_operators_review_later">راجع لاحقًا</string>
|
||||
<string name="error_server_protocol_changed">تغيّر بروتوكول الخادم.</string>
|
||||
<string name="share_address_publicly">شارك العنوان علناً</string>
|
||||
<string name="share_simplex_address_on_social_media">شارك عنوان SimpleX على وسائل التواصل الاجتماعي.</string>
|
||||
<string name="simplex_address_and_1_time_links_are_safe_to_share">عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة.</string>
|
||||
<string name="address_creation_instruction">انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">حُذفت هذه الرسالة أو لم تُستلم بعد.</string>
|
||||
<string name="operator_use_for_messages">استخدم للرسائل</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة.</string>
|
||||
@@ -2191,7 +2116,6 @@
|
||||
<string name="add_your_team_members_to_conversations">أضف أعضاء فريقك إلى المحادثات.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">يُمنع إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>أجهزة Xiaomi</b>: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[تُرسل جميع الرسائل والملفات <b>مُعمَّاة بين الطرفين</b>، مع أمان ما بعد الكم في الرسائل المباشرة.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc_short">تحقق من الرسائل كل 10 دقائق</string>
|
||||
<string name="direct_messages_are_prohibited">يُمنع إرسال الرسائل المباشرة بين الأعضاء.</string>
|
||||
<string name="info_row_chat">الدردشة</string>
|
||||
@@ -2207,7 +2131,6 @@
|
||||
<string name="onboarding_notifications_mode_battery">الإشعارات والبطارية</string>
|
||||
<string name="only_chat_owners_can_change_prefs">فقط مالكي الدردشة يمكنهم تغيير التفضيلات.</string>
|
||||
<string name="v6_2_business_chats_descr">الخصوصية لعملائك.</string>
|
||||
<string name="remote_hosts_section">الجوّالات عن بُعد</string>
|
||||
<string name="invite_to_chat_button">ادعُ للدردشة</string>
|
||||
<string name="leave_chat_question">مغادرة المجموعة؟</string>
|
||||
<string name="member_will_be_removed_from_chat_cannot_be_undone">سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا!</string>
|
||||
@@ -2287,7 +2210,6 @@
|
||||
<string name="report_compose_reason_header_spam">بلّغ عن إزعاج (spam): سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="report_archive_alert_title">أرشفة البلاغ؟</string>
|
||||
<string name="report_reason_alert_title">سبب الإبلاغ؟</string>
|
||||
<string name="report_archive_alert_desc">سيتم أرشفة البلاغ لك.</string>
|
||||
<string name="blocking_reason_spam">إزعاج (spam)</string>
|
||||
<string name="privacy_chat_list_open_links_yes">نعم</string>
|
||||
<string name="privacy_chat_list_open_links_ask">اسأل</string>
|
||||
@@ -2312,7 +2234,6 @@
|
||||
<string name="network_smp_web_port_toggle">استخدم منفذ الويب</string>
|
||||
<string name="network_smp_web_port_section_title">منفذ TCP للمُراسلة</string>
|
||||
<string name="mute_all_chat">اكتم الكل</string>
|
||||
<string name="unread_mentions">ذّكورات غير مقروءة</string>
|
||||
<string name="max_group_mentions_per_message_reached">يمكنك ذكر ما يصل إلى %1$s من الأعضاء في الرسالة الواحدة!</string>
|
||||
<string name="enable_sending_member_reports">السماح بالإبلاغ عن الرسائل إلى المُشرفين.</string>
|
||||
<string name="disable_sending_member_reports">امنع الإبلاغ عن الرسائل للمُشرفين.</string>
|
||||
@@ -2438,7 +2359,6 @@
|
||||
<string name="compose_view_send_contact_request_alert_question">إرسال طلب جهة اتصال؟</string>
|
||||
<string name="compose_view_send_request">أرسل طلب</string>
|
||||
<string name="compose_view_send_request_without_message">أرسل طلب دون رسالة</string>
|
||||
<string name="sent_to_your_contact_after_connection">أُرسل إلى جهة اتصالك بعد الاتصال.</string>
|
||||
<string name="share_group_profile_via_link">ترقية رابط المجموعة؟</string>
|
||||
<string name="share_profile_via_link_alert_confirm">رقِّ</string>
|
||||
<string name="share_profile_via_link">ترقية العنوان؟</string>
|
||||
@@ -2494,7 +2414,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">طلبات التواصل في المجموعات</string>
|
||||
<string name="member_is_deleted_cant_accept_request">حُذف العضو - لا يمكن قبول الطلب</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">طُلب اتصال من المجموعة %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">هذا الإعداد لملف تعريفك الحالي</string>
|
||||
<string name="allow_files_and_media_only_if">اسمح بالملفات والوسائط فقط إذا سمح جهة اتصالك بذلك.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">اسمح لجهات اتصالك بإرسال الملفات والوسائط.</string>
|
||||
<string name="chat_banner_bot">روبوت</string>
|
||||
@@ -2526,7 +2445,6 @@
|
||||
<string name="info_row_connection_failed">فشل الاتصال</string>
|
||||
<string name="member_info_member_failed">فشل</string>
|
||||
<string name="content_filter_files">ملفات</string>
|
||||
<string name="content_filter_menu_item">تصفية</string>
|
||||
<string name="content_filter_images">صور</string>
|
||||
<string name="content_filter_links">روابط</string>
|
||||
<string name="placeholder_search_files">ابحث عن ملفات</string>
|
||||
@@ -2562,7 +2480,6 @@
|
||||
<string name="info_row_channel">قناة</string>
|
||||
<string name="channel_full_name_field">اسم القناة بالكامل</string>
|
||||
<string name="channel_link">رابط القناة</string>
|
||||
<string name="button_channel_members">أعضاء القناة</string>
|
||||
<string name="channel_display_name_field">اسم القناة</string>
|
||||
<string name="channel_profile_is_stored_on_subscribers_devices">يُخزّن ملف تعريف القناة على أجهزة المشتركين وعلى مُرحلات الدردشة.</string>
|
||||
<string name="snd_channel_event_channel_profile_updated">حُدِّث ملف تعريف القناة</string>
|
||||
@@ -2590,7 +2507,6 @@
|
||||
<string name="enter_relay_name">أدخل اسم المُرحل…</string>
|
||||
<string name="error_adding_relay">خطأ في إضافة المُرحل</string>
|
||||
<string name="error_creating_channel">خطأ في إنشاء القناة</string>
|
||||
<string name="error_opening_channel">خطأ في فتح القناة</string>
|
||||
<string name="rcv_msg_error_parse">خطأ: %s</string>
|
||||
<string name="error_saving_channel_profile">خطأ في حفظ ملف تعريف القناة</string>
|
||||
<string name="relay_conn_status_failed">فشل</string>
|
||||
@@ -2610,7 +2526,6 @@
|
||||
<string name="no_chat_relays_enabled">لا مُرحلات دردشة مفعّلة.</string>
|
||||
<string name="not_all_relays_connected">ليس كل المُرحلات متصلة</string>
|
||||
<string name="connect_plan_open_channel">افتح قناة</string>
|
||||
<string name="connect_plan_open_new_channel">افتح قناة جديدة</string>
|
||||
<string name="member_info_section_title_owner">المالك</string>
|
||||
<string name="channel_members_section_owners">المالكين والمساهمين</string>
|
||||
<string name="preset_relay_address">عنوان المُرحل مسبق الضبط</string>
|
||||
@@ -2779,7 +2694,6 @@
|
||||
<string name="your_public_address">عنوانك العام</string>
|
||||
<string name="why_built_heading">لقد وُلدت دون حساب تعريفي.</string>
|
||||
<string name="cancel_channel_alert_msg">قناتك الجديدة %1$s متصلة بـ %2$d من أصل %3$d مُرحلات. إذا ألغيت، ستُحذف القناة - يمكنك إنشاؤها مرة أخرى.</string>
|
||||
<string name="add_button">أضف</string>
|
||||
<string name="add_relay_button">أضف مُرحل</string>
|
||||
<string name="add_relays_title">أضف مُرحلات</string>
|
||||
<string name="button_cancel_and_delete_channel">ألغِ واحذف القناة</string>
|
||||
@@ -2814,14 +2728,11 @@
|
||||
<string name="member_info_status">الحالة</string>
|
||||
<string name="channel_owner_count_singular">%1$d مالك</string>
|
||||
<string name="channel_owner_count_plural">%1$d مالكون</string>
|
||||
<string name="settings_section_title_about">عن</string>
|
||||
<string name="advanced_options">خيارات متقدّمة</string>
|
||||
<string name="advanced_settings">إعدادات متقدّمة</string>
|
||||
<string name="allow_anyone_to_embed">اسمح لأي شخص بالتضمين</string>
|
||||
<string name="embed_any_webpage_can_show">يمكن لأي صفحة ويب عرض المعاينة.</string>
|
||||
<string name="chat_data">بيانات الدردشة</string>
|
||||
<string name="channel_name_requires_newer_app_version">يتطلب الاتصال عبر اسم القناة إصدارًا أحدث من التطبيق.</string>
|
||||
<string name="contact_name_requires_newer_app_version">يتطلب الاتصال عبر اسم جهة الاتصال إصدارًا أحدث من التطبيق.</string>
|
||||
<string name="settings_section_title_contact">تواصل</string>
|
||||
<string name="group_member_role_member_channel">مساهم</string>
|
||||
<string name="copy_code">انسخ الرمز</string>
|
||||
@@ -2833,7 +2744,6 @@
|
||||
<string name="webpage_url_footer">سيتم عرضه للمشتركين واستخدامه للسماح بتحميل المعاينة.</string>
|
||||
<string name="more_privacy">مزيد من الخصوصية</string>
|
||||
<string name="embed_only_your_page">لا يمكن عرض المعاينة إلا على صفحتك المذكورة أعلاه.</string>
|
||||
<string name="please_upgrade_the_app">يُرجى ترقية التطبيق.</string>
|
||||
<string name="channel_owners_contributors_count">%1$d مالكين ومساهمين</string>
|
||||
<string name="badge_supported_simplex">%1$s دعم تطبيق SimpleX Chat. انتهت صلاحية الشارة في %2$s.</string>
|
||||
<string name="relay_status_acknowledged_roster">قائمة المعترف بهم</string>
|
||||
@@ -2849,8 +2759,6 @@
|
||||
<string name="member_role_will_be_changed_with_notification_channel">سيتم تغيير الدور إلى "%s". وسيتم إشعار جميع المشاركين في القناة بذلك.</string>
|
||||
<string name="badge_unverified_desc">تعذّر التحقق من صحة هذه الشارة، وقد لا تكون أصلية.</string>
|
||||
<string name="group_link_requires_newer_version">تتطلب هذه المجموعة إصدارًا أحدث من التطبيق. يُرجى تحديث التطبيق للانضمام إليها.</string>
|
||||
<string name="unsupported_channel_name">اسم قناة غير مدعوم</string>
|
||||
<string name="unsupported_contact_name">اسم جهة اتصال غير مدعوم</string>
|
||||
<string name="badge_unverified_title">شارة غير متحقق منها</string>
|
||||
<string name="relays_no_web_support">لا تدعم خوادم ترحيل الدردشة المستعملة صفحات الويب.</string>
|
||||
<string name="webpage_code">رمز صفحة الويب</string>
|
||||
@@ -2865,7 +2773,6 @@
|
||||
<string name="simplex_name_resolver_error_desc">خطأ المحلّل: %1$s</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">الخادم %1$s لا يدعم تحليل الأسماء. اضبط الخوادم أو استخدم رابط الاتصال.</string>
|
||||
<string name="set_simplex_name">عيِّن اسم SimpleX</string>
|
||||
<string name="simplex_name">اسم SimpleX</string>
|
||||
<string name="simplex_name_error">خطأ في اسم SimpleX</string>
|
||||
<string name="simplex_name_not_verified">لم يتحقق مِن اسم SimpleX</string>
|
||||
<string name="simplex_name_no_valid_link_desc">اسم SimpleX %1$s مسجَّل، لكنه لا يحتوي على رابط صالح.</string>
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
<string name="connect_use_incognito_profile">Use incognito profile</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Your profile will be sent to the contact that you received this link from.</string>
|
||||
<string name="you_will_join_group">You will connect to all group members.</string>
|
||||
<string name="connect_via_link_verb">Connect</string>
|
||||
<string name="connect_via_link_incognito">Connect incognito</string>
|
||||
<string name="connect_plan_open_chat">Open chat</string>
|
||||
<string name="connect_plan_join_name">Join channel %s</string>
|
||||
<string name="connect_plan_connect_to_name">Connect to %s</string>
|
||||
@@ -128,12 +126,6 @@
|
||||
<string name="report_reason_other">Another reason</string>
|
||||
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Error saving SMP servers</string>
|
||||
<string name="error_saving_xftp_servers">Error saving XFTP servers</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Make sure XFTP server addresses are in correct format, line separated and are not duplicated.</string>
|
||||
<string name="error_loading_smp_servers">Error loading SMP servers</string>
|
||||
<string name="error_loading_xftp_servers">Error loading XFTP servers</string>
|
||||
<string name="error_setting_network_config">Error updating network configuration</string>
|
||||
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
||||
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
||||
@@ -178,6 +170,7 @@
|
||||
<string name="proxy_destination_error_failed_to_connect">Forwarding server %1$s failed to connect to destination server %2$s. Please try later.</string>
|
||||
<string name="proxy_destination_error_broker_host">Destination server address of %1$s is incompatible with forwarding server %2$s settings.</string>
|
||||
<string name="proxy_destination_error_broker_version">Destination server version of %1$s is incompatible with forwarding server %2$s.</string>
|
||||
<string name="service_request_timeout_desc">The service did not respond. Please try again.</string>
|
||||
<string name="please_try_later">Please try later.</string>
|
||||
<string name="error_sending_message">Error sending message</string>
|
||||
<string name="error_forwarding_messages">Error forwarding messages</string>
|
||||
@@ -203,11 +196,6 @@
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
|
||||
<string name="unsupported_connection_link">Unsupported connection link</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</string>
|
||||
<string name="unsupported_channel_name">Unsupported channel name</string>
|
||||
<string name="unsupported_contact_name">Unsupported contact name</string>
|
||||
<string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string>
|
||||
<string name="please_upgrade_the_app">Please upgrade the app.</string>
|
||||
<string name="simplex_name_error">SimpleX name error</string>
|
||||
<string name="simplex_name_no_servers_desc">None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">Server %1$s does not support name resolution. Configure servers, or use a connection link.</string>
|
||||
@@ -373,7 +361,6 @@
|
||||
<string name="report_archive_alert_title">Archive report?</string>
|
||||
<string name="report_archive_alert_title_nth">Archive %d reports?</string>
|
||||
<string name="report_archive_alert_title_all">Archive all reports?</string>
|
||||
<string name="report_archive_alert_desc">The report will be archived for you.</string>
|
||||
<string name="report_archive_alert_desc_all">All reports will be archived for you.</string>
|
||||
<string name="report_archive_for_me">For me</string>
|
||||
<string name="report_archive_for_all_moderators">For all moderators</string>
|
||||
@@ -424,7 +411,6 @@
|
||||
<string name="content_filter_files">Files</string>
|
||||
<string name="content_filter_links">Links</string>
|
||||
<string name="content_filter_all_messages">All messages</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="archive_verb">Archive</string>
|
||||
<string name="archive_report">Archive report</string>
|
||||
<string name="archive_reports">Archive reports</string>
|
||||
@@ -492,8 +478,6 @@
|
||||
|
||||
<!-- ChatListView.kt -->
|
||||
<string name="personal_welcome">Welcome %1$s!</string>
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Settings</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
@@ -503,7 +487,6 @@
|
||||
<string name="group_preview_join_as">Join as %s</string>
|
||||
<string name="group_preview_rejected">rejected</string>
|
||||
<string name="group_connection_pending">connecting…</string>
|
||||
<string name="tap_to_start_new_chat">Tap to start a new chat</string>
|
||||
<string name="chat_with_developers">Chat with the developers</string>
|
||||
<string name="you_have_no_chats">You have no chats</string>
|
||||
<string name="talk_to_someone">Talk to someone</string>
|
||||
@@ -528,7 +511,6 @@
|
||||
<string name="contact_should_accept">contact should accept…</string>
|
||||
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
||||
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
||||
<string name="address_creation_instruction">Tap Create SimpleX address in the menu to create it later.</string>
|
||||
|
||||
<!-- ChatView.kt -->
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
@@ -687,6 +669,15 @@
|
||||
<string name="icon_descr_file">File</string>
|
||||
<string name="large_file">Large file!</string>
|
||||
<string name="contact_sent_large_file">Your contact sent a file that is larger than currently supported maximum size (%1$s).</string>
|
||||
<string name="supporter_badge">supporter badge</string>
|
||||
<string name="legend_badge">legend badge</string>
|
||||
<string name="large_file_requires_badge">You need a %1$s to send files larger than %2$s.</string>
|
||||
<string name="max_file_size_with_badge">Maximum supported file size is %1$s, with a %2$s.</string>
|
||||
<string name="badge_expired">Contact\'s badge expired.</string>
|
||||
<string name="badge_verification_failed">Contact\'s badge verification failed.</string>
|
||||
<string name="badge_no_key">No key to verify contact\'s badge.</string>
|
||||
<string name="your_badge_expired">Your badge expired.</string>
|
||||
<string name="large_file_incognito">Files larger than %1$s cannot be sent in incognito chats.</string>
|
||||
<string name="maximum_supported_file_size">Currently maximum supported file size is %1$s.</string>
|
||||
<string name="waiting_for_file">Waiting for file</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">File will be received when your contact completes uploading it.</string>
|
||||
@@ -717,8 +708,6 @@
|
||||
<string name="chat_ttl_options_footer">Delete chat messages from your device.</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="info_view_connect_button">connect</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_call_button">call</string>
|
||||
<string name="info_view_search_button">search</string>
|
||||
@@ -741,7 +730,6 @@
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
<string name="icon_descr_server_status_error">Error</string>
|
||||
<string name="icon_descr_server_status_pending">Pending</string>
|
||||
<string name="switch_receiving_address_question">Change receiving address?</string>
|
||||
<string name="switch_receiving_address_desc">Receiving address will be changed to a different server. Address change will complete after sender comes online.</string>
|
||||
<string name="abort_switch_receiving_address_question">Abort changing address?</string>
|
||||
@@ -803,12 +791,8 @@
|
||||
|
||||
<!-- NewChatSheet -->
|
||||
<string name="add_contact_or_create_group">Start new chat</string>
|
||||
<string name="share_one_time_link">Create one-time invitation link</string>
|
||||
<string name="connect_via_link_or_qr">Connect via link / QR code</string>
|
||||
<string name="scan_QR_code">Scan QR code</string>
|
||||
<string name="create_group">Create secret group</string>
|
||||
<string name="to_share_with_your_contact">(to share with your contact)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</string>
|
||||
<string name="only_stored_on_members_devices">(only stored by group members)</string>
|
||||
|
||||
<!-- QRCodeScanner -->
|
||||
@@ -871,7 +855,6 @@
|
||||
<string name="unmute_chat">Unmute</string>
|
||||
<string name="favorite_chat">Favorite</string>
|
||||
<string name="unfavorite_chat">Unfavorite</string>
|
||||
<string name="unread_mentions">Unread mentions</string>
|
||||
|
||||
<!-- Tags - ChatListNavLinkView.kt -->
|
||||
<string name="create_list">Create list</string>
|
||||
@@ -921,19 +904,12 @@
|
||||
<string name="show_QR_code">Show QR code</string>
|
||||
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Invalid QR code</string>
|
||||
<string name="this_QR_code_is_not_a_link">This QR code is not a link!</string>
|
||||
<string name="invalid_contact_link">Invalid link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">This link is not a valid connection link!</string>
|
||||
<string name="connection_request_sent">Connection request sent!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">You will be connected to group when the group host\'s device is online, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">You will be connected when your connection request is accepted, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[If you can\'t meet in person, <b>show QR code in the video call</b>, or share the link.]]></string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Your chat profile will be sent\nto your contact</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.]]></string>
|
||||
<string name="share_invitation_link">Share 1-time link</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Paste the link you received to connect with your contact…</string>
|
||||
<string name="learn_more">Learn more</string>
|
||||
<string name="learn_more_about_address">About SimpleX address</string>
|
||||
|
||||
@@ -947,7 +923,6 @@
|
||||
<!-- User Address Learn More - UserAddressLearnMore.kt -->
|
||||
<string name="share_address_publicly">Share address publicly</string>
|
||||
<string name="share_simplex_address_on_social_media">Share SimpleX address on social media.</string>
|
||||
<string name="you_can_share_your_address">You can share your address as a link or QR code - anybody can connect to you.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">You won\'t lose your contacts if you later delete your address.</string>
|
||||
<string name="share_1_time_link_with_a_friend">Share 1-time link with a friend</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1-time link can be used <i>with one contact only</i> - share in person or via any messenger.]]></string>
|
||||
@@ -955,7 +930,6 @@
|
||||
<string name="connection_security">Connection security</string>
|
||||
<string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX address and 1-time links are safe to share via any messenger.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">To protect against your link being replaced, you can compare contact security codes.</string>
|
||||
<string name="you_can_accept_or_reject_connection">When people request to connect, you can accept or reject it.</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Read more in <font color="#0088ff">User Guide</font>.]]></string>
|
||||
<string name="address_or_1_time_link">Address or 1-time link?</string>
|
||||
|
||||
@@ -964,7 +938,6 @@
|
||||
<string name="connect_button">Connect</string>
|
||||
<string name="paste_button">Paste</string>
|
||||
<string name="this_string_is_not_a_connection_link">This string is not a connection link!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.]]></string>
|
||||
|
||||
<!-- NewChatView.kt -->
|
||||
<string name="new_chat">New chat</string>
|
||||
@@ -978,7 +951,6 @@
|
||||
<string name="verify_simplex_name_action">Verify name</string>
|
||||
<string name="verify_simplex_names">Verify SimpleX names</string>
|
||||
<string name="simplex_name_not_verified">SimpleX name not verified</string>
|
||||
<string name="simplex_name">SimpleX name</string>
|
||||
<string name="your_simplex_name">Your SimpleX name</string>
|
||||
<string name="set_simplex_name">Set SimpleX name</string>
|
||||
<string name="error_saving_simplex_name">Error saving name</string>
|
||||
@@ -1046,11 +1018,7 @@
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="message_servers">Message servers</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="smp_servers_configured">Configured SMP servers</string>
|
||||
<string name="smp_servers_other">Other SMP servers</string>
|
||||
<string name="smp_servers_preset_address">Preset server address</string>
|
||||
<string name="smp_servers_preset_add">Add preset servers</string>
|
||||
<string name="smp_servers_add">Add server</string>
|
||||
<string name="smp_servers_test_server">Test server</string>
|
||||
<string name="smp_servers_test_servers">Test servers</string>
|
||||
@@ -1060,8 +1028,6 @@
|
||||
<string name="smp_servers_scan_qr">Scan server QR code</string>
|
||||
<string name="smp_servers_enter_manually">Enter server manually</string>
|
||||
<string name="smp_servers_new_server">New server</string>
|
||||
<string name="smp_servers_preset_server">Preset server</string>
|
||||
<string name="smp_servers_your_server">Your server</string>
|
||||
<string name="smp_servers_your_server_address">Your server address</string>
|
||||
<string name="smp_servers_use_server">Use server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Use for new connections</string>
|
||||
@@ -1072,9 +1038,6 @@
|
||||
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
|
||||
<string name="smp_save_servers_question">Save servers?</string>
|
||||
<string name="media_and_file_servers">Media & file servers</string>
|
||||
<string name="xftp_servers">XFTP servers</string>
|
||||
<string name="xftp_servers_configured">Configured XFTP servers</string>
|
||||
<string name="xftp_servers_other">Other XFTP servers</string>
|
||||
<string name="subscription_percentage">Show percentage</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install SimpleX Chat for terminal</string>
|
||||
<string name="reset_all_hints">Reset all hints</string>
|
||||
@@ -1082,8 +1045,6 @@
|
||||
<string name="contribute">Contribute</string>
|
||||
<string name="rate_the_app">Rate the app</string>
|
||||
<string name="use_simplex_chat_servers__question">Use SimpleX Chat servers?</string>
|
||||
<string name="your_SMP_servers">Your SMP servers</string>
|
||||
<string name="your_XFTP_servers">Your XFTP servers</string>
|
||||
<string name="using_simplex_chat_servers">Using SimpleX Chat servers.</string>
|
||||
<string name="how_to">How to</string>
|
||||
<string name="how_to_use_your_servers">How to use your servers</string>
|
||||
@@ -1133,7 +1094,6 @@
|
||||
<string name="network_session_mode_session_description">New SOCKS credentials will be used every time you start the app.</string>
|
||||
<string name="network_session_mode_server_description">New SOCKS credentials will be used for each server.</string>
|
||||
<string name="network_session_mode_entity_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]></string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
|
||||
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls use direct connection.]]></string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Private routing</string>
|
||||
@@ -1145,7 +1105,6 @@
|
||||
<string name="network_smp_proxy_mode_unknown_description">Use private routing with unknown servers.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Use private routing with unknown servers when IP address is not protected.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Do NOT use private routing.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Message routing mode</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Allow downgrade</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Yes</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">When IP hidden</string>
|
||||
@@ -1153,7 +1112,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow_description">Send messages directly when your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Send messages directly when IP address is protected and your or destination server does not support private routing.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Do NOT send messages directly, even if your or destination server does not support private routing.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Message routing fallback</string>
|
||||
<string name="private_routing_show_message_status">Show message status</string>
|
||||
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
|
||||
<string name="network_smp_web_port_section_title">TCP port for messaging</string>
|
||||
@@ -1211,7 +1169,6 @@
|
||||
<string name="all_your_contacts_will_remain_connected_update_sent">All your contacts will remain connected. Profile update will be sent to your contacts.</string>
|
||||
<string name="share_link">Share link</string>
|
||||
<string name="add_address_to_your_profile">Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts.</string>
|
||||
<string name="create_address_and_let_people_connect">Create an address to let people connect with you.</string>
|
||||
<string name="create_simplex_address">Create SimpleX address</string>
|
||||
<string name="share_with_contacts">Share with SimpleX contacts</string>
|
||||
<string name="share_address_with_contacts_question">Share address with SimpleX contacts?</string>
|
||||
@@ -1220,7 +1177,6 @@
|
||||
<string name="stop_sharing">Stop sharing</string>
|
||||
<string name="auto_accept_contact">Auto-accept</string>
|
||||
<string name="group_invitations">Group invitations</string>
|
||||
<string name="sent_to_your_contact_after_connection">Sent to your contact after connection.</string>
|
||||
<string name="address_welcome_message">Welcome message</string>
|
||||
<string name="enter_welcome_message_optional">Enter welcome message… (optional)</string>
|
||||
<string name="save_settings_question">Save settings?</string>
|
||||
@@ -1253,9 +1209,6 @@
|
||||
|
||||
<!-- CreateSimpleXAddress.kt -->
|
||||
<string name="continue_to_next_step">Continue</string>
|
||||
<string name="dont_create_address">Don\'t create address</string>
|
||||
<string name="you_can_create_it_later">You can create it later</string>
|
||||
<string name="you_can_make_address_visible_via_settings">You can make it visible to your SimpleX contacts via Settings.</string>
|
||||
<string name="invite_friends_short">Invite</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
@@ -1289,16 +1242,12 @@
|
||||
<string name="error_saving_user_password">Error saving user password</string>
|
||||
|
||||
<!-- Welcome Prompts - WelcomeView.kt -->
|
||||
<string name="you_control_your_chat">You control your chat!</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">The messaging and application platform protecting your privacy and security.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">We don\'t store any of your contacts or messages (once delivered) on the servers.</string>
|
||||
<string name="create_profile">Create profile</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Your profile, contacts and delivered messages are stored on your device.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">The profile is only shared with your contacts.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Display name cannot contain whitespace.</string>
|
||||
<string name="display_name">Enter your name:</string>
|
||||
<string name="short_descr">Your bio:</string>
|
||||
<string name="create_profile_button">Create</string>
|
||||
<string name="create_another_profile_button">Create profile</string>
|
||||
<string name="create_address_button">Create</string>
|
||||
<string name="invalid_name">Invalid name!</string>
|
||||
@@ -1354,13 +1303,6 @@
|
||||
<string name="error_initializing_web_view_wrong_arch">Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">The future of messaging</string>
|
||||
<string name="privacy_redefined">Privacy redefined</string>
|
||||
<string name="first_platform_without_user_ids">No user identifiers.</string>
|
||||
<string name="immune_to_spam_and_abuse">Immune to spam</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">You decide who can connect.</string>
|
||||
<string name="decentralized">Decentralized</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Anybody can host servers.</string>
|
||||
<string name="create_your_profile">Create your profile</string>
|
||||
<string name="make_private_connection">Make a private connection</string>
|
||||
<string name="migrate_from_another_device">Migrate from another device</string>
|
||||
@@ -1386,13 +1328,8 @@
|
||||
<string name="why_built_p6">The oldest human freedom — to speak to another person without being watched — built on infrastructure that cannot betray it.</string>
|
||||
<string name="why_built_p7">Because we destroyed the power to know who you are. So that your power can never be taken.</string>
|
||||
<string name="why_built_tagline">Be free in your network.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">To protect your privacy, SimpleX uses separate IDs for each of your contacts.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Only client devices store user profiles, contacts, groups, and messages.</string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[All messages and files are sent <b>end-to-end encrypted</b>, with post-quantum security in direct messages.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Read more in our <font color="#0088ff">GitHub repository</font>.]]></string>
|
||||
|
||||
<!-- SetNotificationsMode.kt -->
|
||||
<string name="use_chat">Use chat</string>
|
||||
<string name="onboarding_notifications_mode_title">Private notifications</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">How it affects battery</string>
|
||||
<string name="onboarding_notifications_mode_off">When app is running</string>
|
||||
@@ -1423,13 +1360,8 @@
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</string>
|
||||
<string name="onboarding_network_about_operators">About operators</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Select network operators to use.</string>
|
||||
<string name="how_it_helps_privacy">How it helps privacy</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">You can configure servers via settings.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Conditions will be accepted for enabled operators after 30 days.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">You can configure operators in Network & servers settings.</string>
|
||||
<string name="onboarding_network_operators_review_later">Review later</string>
|
||||
<string name="onboarding_network_operators_update">Update</string>
|
||||
<string name="onboarding_network_operators_continue">Continue</string>
|
||||
|
||||
<!-- Onboarding Step 3: Your Network -->
|
||||
@@ -1467,7 +1399,6 @@
|
||||
<string name="accept_call_on_lock_screen">Accept</string>
|
||||
<string name="show_call_on_lock_screen">Show</string>
|
||||
<string name="no_call_on_lock_screen">Disable</string>
|
||||
<string name="your_ice_servers">Your ICE servers</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE servers</string>
|
||||
<string name="relay_server_protects_ip">Relay server protects your IP address, but it can observe the duration of the call.</string>
|
||||
<string name="relay_server_if_necessary">Relay server is only used if necessary. Another party can observe your IP address.</string>
|
||||
@@ -1570,7 +1501,6 @@
|
||||
<string name="empty_chat_profile_is_created">An empty chat profile with the provided name is created, and the app opens as usual.</string>
|
||||
<string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string>
|
||||
<string name="set_passcode">Set passcode</string>
|
||||
<string name="this_setting_is_for_your_current_profile">This setting is for your current profile</string>
|
||||
<string name="these_settings_are_for_your_current_profile">These settings are for your current profile</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact and group settings.</string>
|
||||
<string name="receipts_section_contacts">Contacts</string>
|
||||
@@ -1617,7 +1547,6 @@
|
||||
<string name="settings_section_title_files">Files</string>
|
||||
<string name="settings_section_title_delivery_receipts">Send delivery receipts to</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Contact requests in groups</string>
|
||||
<string name="settings_section_title_about">About</string>
|
||||
<string name="settings_section_title_contact">Contact</string>
|
||||
<string name="settings_section_title_support_project">Support the project</string>
|
||||
<string name="chat_data">Chat data</string>
|
||||
@@ -1628,7 +1557,6 @@
|
||||
<string name="settings_shutdown">Shutdown</string>
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
<string name="settings_experimental_features">Experimental features</string>
|
||||
<string name="settings_section_title_socks">SOCKS proxy</string>
|
||||
<string name="settings_section_title_interface" translatable="false">Interface</string>
|
||||
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
|
||||
<string name="settings_section_title_icon">App icon</string>
|
||||
@@ -1651,7 +1579,6 @@
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Your chat database</string>
|
||||
<string name="run_chat_section">Run chat</string>
|
||||
<string name="remote_hosts_section">Remote mobiles</string>
|
||||
<string name="chat_is_running">Chat is running</string>
|
||||
<string name="chat_is_stopped">Chat is stopped</string>
|
||||
<string name="chat_database_section">Chat database</string>
|
||||
@@ -1785,7 +1712,6 @@
|
||||
<string name="one_hand_ui_top_bar">Top bar</string>
|
||||
<string name="one_hand_ui_change_instruction">You can change it in Appearance settings.</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
<string name="upgrade_and_open_chat">Upgrade and open chat</string>
|
||||
<string name="downgrade_and_open_chat">Downgrade and open chat</string>
|
||||
@@ -2028,7 +1954,6 @@
|
||||
<string name="action_button_add_members">Invite</string>
|
||||
<string name="action_button_channel_link">Link</string>
|
||||
<string name="button_support_chat">Chat with admins</string>
|
||||
<string name="button_channel_members">Channel members</string>
|
||||
<string name="button_channel_relays">Chat relays</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
@@ -2199,7 +2124,6 @@
|
||||
<string name="operator">Operator</string>
|
||||
<string name="operator_servers_title">%s servers</string>
|
||||
<string name="operator_info_title">Network operator</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="operator_conditions_accepted_on">Conditions accepted on: %s.</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Conditions will be accepted on: %s.</string>
|
||||
<string name="operator_use_operator_toggle_description">Use servers</string>
|
||||
@@ -2378,7 +2302,6 @@
|
||||
<string name="simplex_links">SimpleX links</string>
|
||||
<string name="recent_history">Visible history</string>
|
||||
<string name="audio_video_calls">Audio/video calls</string>
|
||||
<string name="available_in_v51">\nAvailable in v5.1</string>
|
||||
<string name="feature_enabled">enabled</string>
|
||||
<string name="feature_enabled_for_you">enabled for you</string>
|
||||
<string name="feature_enabled_for_contact">enabled for contact</string>
|
||||
@@ -2705,7 +2628,6 @@
|
||||
<string name="v6_2_network_decentralization">Network decentralization</string>
|
||||
<string name="v6_2_network_decentralization_descr">The second preset operator in the app!</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Enable Flux in Network & servers settings for better metadata privacy.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">for better metadata privacy.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Improved chat navigation</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- Open chat on the first unread message.\n- Jump to quoted messages.</string>
|
||||
<string name="v6_2_business_chats">Business chats</string>
|
||||
@@ -2756,6 +2678,8 @@
|
||||
<string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string>
|
||||
<string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string>
|
||||
<string name="invest_banner_title" translatable="false">Get a stake in SimpleX Chat!</string>
|
||||
<string name="invest_banner_subtitle" translatable="false">Invest on Wefunder from $100</string>
|
||||
<string name="v7_0_simplex_names">SimpleX public names (BETA)</string>
|
||||
<string name="v7_0_simplex_names_descr">Public names for your channel or business.</string>
|
||||
<string name="v7_0_channels">Better channels 📢</string>
|
||||
@@ -3047,7 +2971,6 @@
|
||||
<string name="downloaded_files">Downloaded files</string>
|
||||
<string name="download_errors">Download errors</string>
|
||||
<string name="server_address">Server address</string>
|
||||
<string name="open_server_settings_button">Open server settings</string>
|
||||
|
||||
<!-- GroupMentions.kt -->
|
||||
<string name="max_group_mentions_per_message_reached">You can mention up to %1$s members per message!</string>
|
||||
@@ -3166,7 +3089,6 @@
|
||||
<string name="your_profile_shared_with_channel_relays">Your profile %1$s will be shared with channel relays and subscribers.\nRelays can access channel messages.</string>
|
||||
<string name="configure_relays">Configure relays</string>
|
||||
<string name="relay_status_failed">failed</string>
|
||||
<string name="add_button">Add</string>
|
||||
<string name="add_relay_button">Add relay</string>
|
||||
<string name="add_relays_title">Add relays</string>
|
||||
<string name="no_available_relays">No available relays</string>
|
||||
@@ -3184,12 +3106,10 @@
|
||||
<string name="relay_address_alert_title">Relay address</string>
|
||||
<string name="relay_address_alert_message">This is a chat relay address, it cannot be used to connect.</string>
|
||||
<string name="connect_plan_open_channel">Open channel</string>
|
||||
<string name="connect_plan_open_new_channel">Open new channel</string>
|
||||
<string name="connect_plan_you_are_subscriber">You are a subscriber</string>
|
||||
<string name="connect_plan_you_are_contributor">You are a contributor</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel">Your channel</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[This is your link for channel <b>%1$s</b>!]]></string>
|
||||
<string name="error_opening_channel">Error opening channel</string>
|
||||
|
||||
<!-- ChatListNavLinkView.kt channel-related -->
|
||||
<string name="unblock_subscriber_for_all_question">Unblock subscriber for all?</string>
|
||||
@@ -3220,4 +3140,94 @@
|
||||
<string name="badge_unverified_desc">This badge could not be verified and may not be genuine.</string>
|
||||
<string name="badge_unknown_key_title">Badge cannot be verified</string>
|
||||
<string name="badge_unknown_key_desc">The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge.</string>
|
||||
<string name="badges_level_supporter">Supporter</string>
|
||||
<string name="badges_level_legend">Legend</string>
|
||||
<string name="badges_level_supporter_files">Send 2GB files</string>
|
||||
<string name="badges_level_legend_files">Send 5GB files</string>
|
||||
<string name="badges_price_monthly">%1$s/month</string>
|
||||
<string name="badges_price_annual">%1$s/year</string>
|
||||
<string name="badges_pay_monthly">Pay %1$s/month</string>
|
||||
<string name="badges_pay_annual">Pay %1$s/year</string>
|
||||
<string name="badges_pay_once">Pay %1$s</string>
|
||||
<string name="badges_price_loading">Loading…</string>
|
||||
<string name="badges_price_unavailable">Not available</string>
|
||||
<string name="badges_savings">Save %1$s</string>
|
||||
<string name="badges_level_supporter_tagline">Optional profile badge\nand 2GB files</string>
|
||||
<string name="badges_level_legend_tagline">Optional profile badge\nand 5GB files</string>
|
||||
<string name="badges_period_one_month">1 month</string>
|
||||
<string name="badges_period_monthly">Monthly</string>
|
||||
<string name="badges_period_annual">Annual</string>
|
||||
<string name="badges_billing_footer_subscribe">Renews on %1$s. Cancel anytime.</string>
|
||||
<string name="badges_billing_footer_one_month">Ends on %1$s.</string>
|
||||
<string name="badges_your_level_title">Your level</string>
|
||||
<string name="badges_support_simplex_title">Support SimpleX</string>
|
||||
<string name="badges_support_simplex_body">Get a badge to send larger files (2-5GB) that stay available longer (7-21 days), and to show it on your profile.</string>
|
||||
<string name="badges_why_simplex_is_built">Why SimpleX is built.</string>
|
||||
<string name="badges_choose_your_level">Choose your level</string>
|
||||
<string name="badges_redeem_code_button">Redeem badge code</string>
|
||||
<string name="badges_continue">Continue</string>
|
||||
<string name="badges_how_it_works_button">How badges protect your privacy</string>
|
||||
<string name="badges_how_it_works_title">How badges protect your privacy</string>
|
||||
<string name="badges_how_it_works_p1">A badge is an anonymous credential stored in your profile on your device. This credential is never sent to anyone.</string>
|
||||
<string name="badges_how_it_works_p2">To prove to a contact or a server that you have a badge, the app generates a new proof that reveals only the badge type and the expiry date, rounded to a week.</string>
|
||||
<string name="badges_how_it_works_p3">Nobody can link two different proofs to each other or to the purchase.</string>
|
||||
<string name="badges_how_it_works_read_more_with_link"><![CDATA[Read more in <font color="#0088ff">our blog</font>.]]></string>
|
||||
<string name="badges_preview_my_nickname">My nickname</string>
|
||||
<string name="badges_banner_title">Support SimpleX</string>
|
||||
<string name="badges_banner_subtitle">Get badge + better files</string>
|
||||
<string name="badges_banner_dismiss_message">You can support SimpleX later in Settings.</string>
|
||||
<string name="badges_purchase_successful">Purchase successful</string>
|
||||
<string name="badges_purchase_pending">Purchase pending</string>
|
||||
<string name="badges_purchase_pending_desc">The purchase is awaiting approval. This build does not deliver purchases approved later.</string>
|
||||
<string name="badges_purchase_error">Purchase error</string>
|
||||
<string name="badges_get_your_code">Get your code</string>
|
||||
<string name="badges_redeem_code_title">Redeem code</string>
|
||||
<string name="badges_redeem_code_body">Paste the code you received.</string>
|
||||
<string name="badges_redeem_code_placeholder" translatable="false">SB-XXXXX-XXXXX-XXXXX-XXXXX</string>
|
||||
<string name="badges_redeem">Redeem</string>
|
||||
<string name="badges_error_title">Cannot redeem code</string>
|
||||
<string name="badges_error_invalid_code">This code is not valid.</string>
|
||||
<string name="badges_error_service_not_configured">This app version cannot redeem badge codes.</string>
|
||||
<string name="badges_error_already_active">This profile already has a badge. Redeem the code on another profile, or once this badge ends.</string>
|
||||
<string name="badges_error_code_invalid">This code was not recognised.</string>
|
||||
<string name="badges_error_code_used">This code has already been used.</string>
|
||||
<string name="badges_error_code_expired">This code has expired.</string>
|
||||
<string name="badges_error_rate_limited">Too many attempts. Please try again later.</string>
|
||||
<string name="badges_error_bad_service_response">The badge service sent an unexpected response: %1$s</string>
|
||||
<string name="badges_error_credential_not_verified">This app version cannot verify this badge. Please update the app.</string>
|
||||
<string name="badges_error_unsupported_version">This app version is too old for the badge service. Please update the app.</string>
|
||||
<string name="badges_error_badge_ended">The code was accepted, but the badge it grants has already ended.</string>
|
||||
<string name="badges_error_unknown">The code could not be redeemed.</string>
|
||||
<string name="badges_code_you_scanned_is_not_badge_code">The code you scanned is not a badge code.</string>
|
||||
<string name="badges_your_badge">Your badge</string>
|
||||
<string name="badges_support_ended">Your badge expired</string>
|
||||
<string name="badges_shown_on_your_profile">shown on your profile</string>
|
||||
<string name="badges_ends">Ends</string>
|
||||
<string name="badges_prepaid_footer">Prepaid months have no billing date. The badge is reissued each month from the balance you already paid for, and ends when it runs out.</string>
|
||||
<string name="badges_type_investor">Investor</string>
|
||||
<string name="badges_support_ended_on">Your badge expired on %1$s.</string>
|
||||
<string name="badges_remind_me_later">Remind me later</string>
|
||||
<string name="badges_dismiss">Dismiss</string>
|
||||
<string name="badges_credential">Credential</string>
|
||||
<string name="badges_credential_status">Status</string>
|
||||
<string name="badges_credential_expires">Expires</string>
|
||||
<string name="badges_credential_months_left">Months left</string>
|
||||
<string name="badges_credential_purchase_id">Purchase ID</string>
|
||||
<string name="badges_copy_purchase_key">Copy purchase key</string>
|
||||
<string name="badges_ledger">Badge ledger</string>
|
||||
<string name="badges_ledger_no_entries">No entries</string>
|
||||
<string name="badges_ledger_date">Date</string>
|
||||
<string name="badges_ledger_balance">Balance</string>
|
||||
<string name="badges_ledger_balance_start">Balance start</string>
|
||||
<string name="badges_ledger_anchor">Anchor</string>
|
||||
<string name="badges_ledger_badge_type">Badge type</string>
|
||||
<string name="badges_ledger_paused_since">Paused since</string>
|
||||
<string name="badges_ledger_entry_id">Entry ID</string>
|
||||
<string name="badges_ledger_invoice_id">Invoice ID</string>
|
||||
<string name="badges_ledger_charge_id">Charge ID</string>
|
||||
<string name="badges_ledger_from_purchase_key">From purchase key</string>
|
||||
<string name="badges_ledger_to_purchase_key">To purchase key</string>
|
||||
<string name="supporter_perks">Supporter perks</string>
|
||||
<string name="v7_1_supporter_badge_title">Supporter badge ❤️</string>
|
||||
<string name="v7_1_supporter_badge_body">Help keep the network running — send files up to 2 GB.</string>
|
||||
</resources>
|
||||
@@ -33,7 +33,6 @@
|
||||
<string name="accept_contact_button">Приеми</string>
|
||||
<string name="accept_contact_incognito_button">Приеми инкогнито</string>
|
||||
<string name="about_simplex_chat">За SimpleX Chat</string>
|
||||
<string name="smp_servers_preset_add">Добави предварително зададени сървъри</string>
|
||||
<string name="smp_servers_add_to_another_device">Добави към друго устройство</string>
|
||||
<string name="network_settings">Разширени мрежови настройки</string>
|
||||
<string name="about_simplex">За SimpleX</string>
|
||||
@@ -48,7 +47,6 @@
|
||||
<string name="accept_feature">Приеми</string>
|
||||
<string name="color_primary_variant">Допълнителен акцент</string>
|
||||
<string name="v4_2_group_links_desc">Админите могат да създадат линкове за присъединяване към групи.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Ако не можете да се срещнете лично, <b>покажете QR кода във видеоразговора</b> или споделете линка.]]></string>
|
||||
<string name="open_simplex_chat_to_accept_call">Отворете SimpleX Chat, за да приемете повикването</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Поддръжка на bluetooth и други подобрения.</string>
|
||||
<string name="relay_server_protects_ip">Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора.</string>
|
||||
@@ -103,7 +101,6 @@
|
||||
<string name="alert_title_msg_bad_hash">Лош хеш на съобщението</string>
|
||||
<string name="no_call_on_lock_screen">Деактивиране</string>
|
||||
<string name="callstatus_calling">повикване…</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Ако не можете да се срещнете лично, можете <b>да сканирате QR код във видеоразговора</b> или вашият контакт може да сподели линк за покана.]]></string>
|
||||
<string name="notifications_mode_service_desc">Услугата във фонов режим винаги работи – известията ще се показват веднага щом съобщенията са налични.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Може да бъде деактивирано през настройките</b> – известията ще продължат да се показват, докато приложението работи.]]></string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat разговори</string>
|
||||
@@ -189,8 +186,6 @@
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Позволи на вашите контакти да изпращат изчезващи съобщения.</string>
|
||||
<string name="chat_preferences_always">винаги</string>
|
||||
<string name="audio_video_calls">Аудио/видео разговори</string>
|
||||
<string name="available_in_v51">"
|
||||
\nДостъпно в v5.1"</string>
|
||||
<string name="color_background">Фон</string>
|
||||
<string name="allow_message_reactions">Позволи реакции на съобщения.</string>
|
||||
<string name="allow_direct_messages">Позволи изпращането на лични съобщения до членовете.</string>
|
||||
@@ -248,7 +243,6 @@
|
||||
<string name="v5_0_app_passcode_descr">Задайте го вместо системната идентификация.</string>
|
||||
<string name="v5_1_self_destruct_passcode">Код за достъп за самоунищожение</string>
|
||||
<string name="v5_2_more_things">Още няколко неща</string>
|
||||
<string name="connect_via_link_verb">Свързване</string>
|
||||
<string name="server_connected">свързан</string>
|
||||
<string name="server_connecting">свързване</string>
|
||||
<string name="connection_local_display_name">връзка %1$d</string>
|
||||
@@ -385,12 +379,9 @@
|
||||
<string name="image_decoding_exception_title">Грешка при декодиране</string>
|
||||
<string name="button_delete_contact">Изтрий контакт</string>
|
||||
<string name="delete_contact_question">Изтрий контакт\?</string>
|
||||
<string name="connect_via_link_or_qr">Свърване чрез линк/QR код</string>
|
||||
<string name="copied">Копирано в клипборда</string>
|
||||
<string name="share_one_time_link">Създай линк за еднократна покана</string>
|
||||
<string name="create_group">Създай тайна група</string>
|
||||
<string name="send_disappearing_message_custom_time">Персонализирано време</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканирай или постави от клипборда)</string>
|
||||
<string name="alert_title_contact_connection_pending">Контактът все още не е свързан!</string>
|
||||
<string name="delete_contact_menu_action">Изтрий</string>
|
||||
<string name="delete_group_menu_action">Изтрий</string>
|
||||
@@ -400,18 +391,15 @@
|
||||
<string name="continue_to_next_step">Продължи</string>
|
||||
<string name="core_version">Версия на ядрото: v%s</string>
|
||||
<string name="create_address">Създай адрес</string>
|
||||
<string name="create_address_and_let_people_connect">Създайте адрес, за да позволите на хората да се свързват с вас.</string>
|
||||
<string name="create_simplex_address">Създай SimpleX адрес</string>
|
||||
<string name="customize_theme_title">Персонализирай темата</string>
|
||||
<string name="developer_options">Идентификатори в базата данни и опция за изолация на транспорта.</string>
|
||||
<string name="delete_address">Изтрий адрес</string>
|
||||
<string name="delete_address__question">Изтрий адрес\?</string>
|
||||
<string name="theme_colors_section_title">Цветове на интерфейса</string>
|
||||
<string name="create_profile_button">Създай</string>
|
||||
<string name="create_profile">Създай профил</string>
|
||||
<string name="delete_image">Изтрий изображение</string>
|
||||
<string name="create_your_profile">Създай профил</string>
|
||||
<string name="decentralized">Децентрализиран</string>
|
||||
<string name="status_contact_has_e2e_encryption">контактът има e2e криптиране</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">контактът няма e2e криптиране</string>
|
||||
<string name="delete_files_and_media_all">Изтрий всички файлове</string>
|
||||
@@ -438,9 +426,7 @@
|
||||
<string name="smp_servers_scan_qr">Сканирай QR кода на сървъра</string>
|
||||
<string name="smp_servers_enter_manually">Въведи сървъра ръчно</string>
|
||||
<string name="smp_servers_delete_server">Изтрий сървър</string>
|
||||
<string name="dont_create_address">Не създавай адрес</string>
|
||||
<string name="display_name__field">Име на профила:</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения.</string>
|
||||
<string name="receipts_contacts_title_disable">Деактивирай потвърждениeто\?</string>
|
||||
<string name="receipts_contacts_enable_keep_overrides">Активиране (запазване на промените)</string>
|
||||
<string name="receipts_contacts_title_enable">Активирай потвърждениeто\?</string>
|
||||
@@ -594,7 +580,6 @@
|
||||
<string name="error_deleting_group">Грешка при изтриване на група</string>
|
||||
<string name="error_deleting_pending_contact_connection">Грешка при изтриване на чакащата контактна връзка</string>
|
||||
<string name="error_saving_ICE_servers">Грешка при запазване на ICE сървърите</string>
|
||||
<string name="icon_descr_server_status_pending">Чакаща връзка със сървъра</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Вашият контакт трябва да бъде онлайн, за да осъществите връзката.
|
||||
\nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк).</string>
|
||||
<string name="error_exporting_chat_database">Грешка при експортиране на базата данни</string>
|
||||
@@ -603,9 +588,6 @@
|
||||
<string name="error_importing_database">Грешка при импортиране на базата данни</string>
|
||||
<string name="error_removing_member">Грешка при отстраняване на член</string>
|
||||
<string name="error_saving_group_profile">Грешка при запазване на профила на групата</string>
|
||||
<string name="error_loading_smp_servers">Грешка при зареждане на SMP сървъри</string>
|
||||
<string name="error_loading_xftp_servers">Грешка при зареждане на XFTP сървъри</string>
|
||||
<string name="error_saving_smp_servers">Грешка при запазване на SMP сървърите</string>
|
||||
<string name="error_joining_group">Грешка при присъединяване към група</string>
|
||||
<string name="error_loading_details">Грешка при зареждане на подробности</string>
|
||||
<string name="error_receiving_file">Грешка при получаване на файл</string>
|
||||
@@ -613,7 +595,6 @@
|
||||
<string name="error_saving_user_password">Грешка при запазване на потребителска парола</string>
|
||||
<string name="error_starting_chat">Грешка при стартиране на чата</string>
|
||||
<string name="error_stopping_chat">Грешка при спиране на чата</string>
|
||||
<string name="error_saving_xftp_servers">Грешка при запазване на XFTP сървърите</string>
|
||||
<string name="error_sending_message">Грешка при изпращане на съобщение</string>
|
||||
<string name="error_setting_address">Грешка при задаване на адрес</string>
|
||||
<string name="failed_to_active_user_title">Грешка при смяна на профил!</string>
|
||||
@@ -694,7 +675,6 @@
|
||||
<string name="only_your_contact_can_send_disappearing">Само вашият контакт може да изпраща изчезващи съобщения.</string>
|
||||
<string name="prohibit_sending_disappearing">Забрани изпращането на изчезващи съобщения.</string>
|
||||
<string name="group_members_can_send_disappearing">Членовете могат да изпращат изчезващи съобщения.</string>
|
||||
<string name="invalid_QR_code">Невалиден QR код</string>
|
||||
<string name="invalid_contact_link">Невалиден линк!</string>
|
||||
<string name="incorrect_code">Неправилен код за сигурност!</string>
|
||||
<string name="smp_servers_invalid_address">Невалиден адрес на сървъра!</string>
|
||||
@@ -764,7 +744,6 @@
|
||||
<string name="how_to">Информация</string>
|
||||
<string name="install_simplex_chat_for_terminal">Инсталирай SimpleX Chat за терминал</string>
|
||||
<string name="how_it_works">Как работи</string>
|
||||
<string name="immune_to_spam_and_abuse">Защитен от спам</string>
|
||||
<string name="ignore">Игнорирай</string>
|
||||
<string name="button_add_members">Покани членове</string>
|
||||
<string name="message_deletion_prohibited">Необратимото изтриване на съобщения е забранено в този чат.</string>
|
||||
@@ -801,7 +780,6 @@
|
||||
<string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string>
|
||||
<string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string>
|
||||
<string name="password_to_show">Парола за показване</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color="#0088ff">GitHub хранилище</font>.]]></string>
|
||||
<string name="onboarding_notifications_mode_off">Когато приложението работи</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Периодично</string>
|
||||
<string name="paste_the_link_you_received">Постави получения линк</string>
|
||||
@@ -842,7 +820,6 @@
|
||||
<string name="info_row_updated_at">Записът е актуализиран на</string>
|
||||
<string name="learn_more">Научете повече</string>
|
||||
<string name="paste_button">Постави</string>
|
||||
<string name="smp_servers_preset_server">Предварително зададен сървър</string>
|
||||
<string name="smp_servers_preset_address">Предварително зададен адрес на сървъра</string>
|
||||
<string name="onboarding_notifications_mode_title">Поверителени известия</string>
|
||||
<string name="la_mode_off">Изключено</string>
|
||||
@@ -875,7 +852,6 @@
|
||||
<string name="only_stored_on_members_devices">(съхранено само на устройствата на членовете на групата)</string>
|
||||
<string name="restore_passphrase_not_found_desc">Паролата не е намерена в KeyStore, моля, въведете я ръчно. Това може да се е случило, ако възстановите данните на приложението с помощта на инструмент за резервни копия. Ако не е така, моля, свържете се с разработчиците.</string>
|
||||
<string name="observer_cant_send_message_desc">Моля, свържете се с груповия администартор.</string>
|
||||
<string name="you_can_accept_or_reject_connection">Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите.</string>
|
||||
<string name="profile_update_will_be_sent_to_contacts">Актуализацията на профила ще бъде изпратена до вашите контакти.</string>
|
||||
<string name="prohibit_message_deletion">Забрани необратимото изтриване на съобщения.</string>
|
||||
<string name="prohibit_direct_messages">Забрани изпращането на лични съобщения до членовете.</string>
|
||||
@@ -937,8 +913,6 @@
|
||||
<string name="thousand_abbreviation">х</string>
|
||||
<string name="opening_database">Отваряне на база данни…</string>
|
||||
<string name="live">НА ЖИВО</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Уверете се, че адресите на SMP сървъра са в правилен формат, разделени на редове и не са дублирани.</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Уверете се, че адресите на XFTP сървъра са в правилен формат, разделени на редове и не са дублирани.</string>
|
||||
<string name="marked_deleted_description">маркирано като изтрито</string>
|
||||
<string name="moderated_description">модерирано</string>
|
||||
<string name="moderated_item_description">модерирано от %s</string>
|
||||
@@ -977,9 +951,6 @@
|
||||
<string name="toast_permission_denied">Разрешение е отказано!</string>
|
||||
<string name="image_descr_profile_image">профилно изображение</string>
|
||||
<string name="icon_descr_profile_image_placeholder">запазено място за профилно изображение</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Всеки може да оперира сървъри.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Вие решавате кой може да се свърже с вас.</string>
|
||||
<string name="privacy_redefined">Поверителността преосмислена</string>
|
||||
<string name="make_private_connection">Добави поверителна връзка</string>
|
||||
<string name="open_verb">Отвори</string>
|
||||
<string name="relay_server_if_necessary">Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.</string>
|
||||
@@ -1036,7 +1007,6 @@
|
||||
<string name="icon_descr_address">SimpleX Адрес</string>
|
||||
<string name="image_descr_simplex_logo">SimpleX Лого</string>
|
||||
<string name="icon_descr_simplex_team">SimpleX Екип</string>
|
||||
<string name="smp_servers">SMP сървъри</string>
|
||||
<string name="share_address_with_contacts_question">Сподели адреса с контактите?</string>
|
||||
<string name="share_link">Сподели линк</string>
|
||||
<string name="share_with_contacts">Сподели с контактите</string>
|
||||
@@ -1093,7 +1063,6 @@
|
||||
<string name="share_image">Сподели медия…</string>
|
||||
<string name="simplex_address">SimpleX адрес</string>
|
||||
<string name="v4_2_security_assessment_desc">Сигурността на SimpleX Chat беше одитирана от Trail of Bits.</string>
|
||||
<string name="settings_section_title_socks">SOCKS прокси</string>
|
||||
<string name="settings_restart_app">Рестартиране</string>
|
||||
<string name="settings_shutdown">Изключване</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Рестартирайте приложението, за да създадете нов чат профил.</string>
|
||||
@@ -1141,7 +1110,6 @@
|
||||
<string name="to_connect_via_link_title">За свързване чрез линк</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Връзката, която приехте, ще бъде отказана!</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Контактът, с когото споделихте този линк, НЯМА да може да се свърже!</string>
|
||||
<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>
|
||||
@@ -1150,18 +1118,13 @@
|
||||
<string name="tap_to_activate_profile">Докосни за активиране на профил.</string>
|
||||
<string name="language_system">Системен</string>
|
||||
<string name="color_title">Заглавие</string>
|
||||
<string name="to_share_with_your_contact">(за споделяне с вашия контакт)</string>
|
||||
<string name="alert_message_no_group">Тази група вече не съществува.</string>
|
||||
<string name="settings_section_title_support">Подкрепете SimpleX Chat</string>
|
||||
<string name="contact_sent_large_file">Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%1$s).</string>
|
||||
<string name="in_developing_desc">Тази функция все още не се поддържа. Опитайте следващата версия.</string>
|
||||
<string name="tap_to_start_new_chat">Докосни за започване на нов чат</string>
|
||||
<string name="this_text_is_available_in_settings">Този текст е достъпен в настройките</string>
|
||||
<string name="images_limit_title">Твърде много изображения!</string>
|
||||
<string name="this_QR_code_is_not_a_link">Този QR код не е линк!</string>
|
||||
<string name="smp_servers_test_server">Тествай сървър</string>
|
||||
<string name="network_session_mode_transport_isolation">Транспортна изолация</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформата за съобщения и приложения, защитаваща вашата поверителност и сигурност.</string>
|
||||
<string name="switch_verb">Смени</string>
|
||||
<string name="v4_4_french_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string>
|
||||
<string name="v5_0_large_files_support">Видео и файлове до 1gb</string>
|
||||
@@ -1179,7 +1142,6 @@
|
||||
<string name="this_string_is_not_a_connection_link">Този текст не е линк за връзка!</string>
|
||||
<string name="videos_limit_title">Твърде много видеоклипове!</string>
|
||||
<string name="messages_section_description">Тази настройка се прилага за съобщения в текущия ви профил</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</string>
|
||||
<string name="error_connecting_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s).</string>
|
||||
<string name="error_smp_test_failed_at_step">Тестът е неуспешен на стъпка %s.</string>
|
||||
@@ -1188,8 +1150,6 @@
|
||||
<string name="chat_help_tap_button">Докосни бутона</string>
|
||||
<string name="thank_you_for_installing_simplex">Благодарим Ви, че инсталирахте SimpleX Chat!</string>
|
||||
<string name="save_and_notify_contacts">Запази и уведоми контактите</string>
|
||||
<string name="next_generation_of_private_messaging">Бъдещето на комуникацията</string>
|
||||
<string name="first_platform_without_user_ids">Няма потребителски идентификатори.</string>
|
||||
<string name="la_mode_system">Системна</string>
|
||||
<string name="alert_text_msg_bad_id">Неправилно ID на следващото съобщение (по-малко или еднакво с предишното).
|
||||
\nТова може да се случи поради някаква грешка или когато връзката е компрометирана.</string>
|
||||
@@ -1203,11 +1163,9 @@
|
||||
<string name="your_settings">Настройки</string>
|
||||
<string name="your_simplex_contact_address">SimpleX адрес</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Използвай за нови връзки</string>
|
||||
<string name="your_XFTP_servers">Вашите XFTP сървъри</string>
|
||||
<string name="use_simplex_chat_servers__question">Използвай сървърите на SimpleX Chat\?</string>
|
||||
<string name="using_simplex_chat_servers">Използват се сървърите на SimpleX Chat.</string>
|
||||
<string name="your_ICE_servers">Вашите ICE сървъри</string>
|
||||
<string name="your_SMP_servers">Вашите SMP сървъри</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Използвай SOCKS прокси</string>
|
||||
<string name="network_enable_socks">Използвай SOCKS прокси\?</string>
|
||||
<string name="network_disable_socks">Използване на директна интернет връзка\?</string>
|
||||
@@ -1215,7 +1173,6 @@
|
||||
<string name="network_use_onion_hosts_prefer">Когато са налични</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Можете да използвате markdown за форматиране на съобщенията:</string>
|
||||
<string name="use_chat">Използвай чата</string>
|
||||
<string name="update_database">Актуализация</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството.</string>
|
||||
<string name="unknown_database_error_with_info">Неизвестна грешка в базата данни: %s</string>
|
||||
@@ -1246,17 +1203,12 @@
|
||||
<string name="voice_message_send_text">Гласово съобщение…</string>
|
||||
<string name="voice_messages_are_prohibited">Гласовите съобщения са забранени.</string>
|
||||
<string name="icon_descr_received_msg_status_unread">непрочетено</string>
|
||||
<string name="welcome">Добре дошли!</string>
|
||||
<string name="personal_welcome">Добре дошли %1$s!</string>
|
||||
<string name="you_have_no_chats">Нямате чатове</string>
|
||||
<string name="observer_cant_send_message_title">вие сте наблюдател</string>
|
||||
<string name="smp_servers_your_server">Вашият сървър</string>
|
||||
<string name="smp_servers_use_server">Използвай сървър</string>
|
||||
<string name="smp_servers_your_server_address">Вашият адрес на сървъра</string>
|
||||
<string name="you_can_create_it_later">Можете да го създадете по-късно</string>
|
||||
<string name="your_contacts_will_remain_connected">Вашите контакти ще останат свързани.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Ние не съхраняваме вашите контакти или съобщения (веднъж доставени) на сървърите.</string>
|
||||
<string name="you_control_your_chat">Вие контролирате своя чат!</string>
|
||||
<string name="wrong_passphrase_title">Грешна парола!</string>
|
||||
<string name="voice_messages">Гласови съобщения</string>
|
||||
<string name="your_preferences">Настройки</string>
|
||||
@@ -1269,11 +1221,8 @@
|
||||
<string name="snd_group_event_member_deleted">премахнахте %1$s</string>
|
||||
<string name="incognito_random_profile">Вашият автоматично генериран профил</string>
|
||||
<string name="user_unmute">Уведомявай</string>
|
||||
<string name="you_can_share_your_address">Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">променихте адреса получаване за %s</string>
|
||||
<string name="group_main_profile_sent">Вашият чат профил ще бъде изпратен на членовете на групата</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Вашият чат профил ще бъде изпратен
|
||||
\nдо вашия контакт</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. \nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Актуализирането на настройките ще свърже отново клиента към всички сървъри.</string>
|
||||
<string name="rcv_group_event_updated_group_profile">актуализиран профил на групата</string>
|
||||
@@ -1309,15 +1258,11 @@
|
||||
<string name="gallery_video_button">Видео</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color="#0088ff">се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string>
|
||||
<string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.]]></string>
|
||||
<string name="xftp_servers">XFTP сървъри</string>
|
||||
<string name="update_network_session_mode_question">Актуализиране на режима на изолация на транспорта\?</string>
|
||||
<string name="your_current_profile">Вашият текущ профил</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.</string>
|
||||
<string name="icon_descr_video_off">Видеото е изключено</string>
|
||||
<string name="icon_descr_video_on">Видеото е включено</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE сървъри</string>
|
||||
<string name="your_ice_servers">Вашите ICE сървъри</string>
|
||||
<string name="your_privacy">Вашата поверителност</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти.</string>
|
||||
<string name="group_info_member_you">вие: %1$s</string>
|
||||
@@ -1327,7 +1272,6 @@
|
||||
<string name="v5_1_better_messages_descr">- гласови съобщения до 5 минути.
|
||||
\n- персонализирано време за изчезване.
|
||||
\n- история на редактиране.</string>
|
||||
<string name="connect_via_link_incognito">Свързване инкогнито</string>
|
||||
<string name="turn_off_system_restriction_button">Отвори настройките на приложението</string>
|
||||
<string name="turn_off_battery_optimization_button">Позволи</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Нов автоматично генериран профил ще бъде споделен.</string>
|
||||
@@ -1335,7 +1279,6 @@
|
||||
<string name="system_restricted_background_in_call_title">Без фонови разговори</string>
|
||||
<string name="connect_via_member_address_alert_title">Свързване директно\?</string>
|
||||
<string name="connect_use_current_profile">Използвай текущия профил</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Поставете линка, който сте получили, за да се свържете с вашия контакт…</string>
|
||||
<string name="connect__your_profile_will_be_shared">Вашият профил %1$s ще бъде споделен.</string>
|
||||
<string name="system_restricted_background_desc">SimpleX не може да работи във фонов режим. Ще получавате известията само когато приложението работи.</string>
|
||||
<string name="system_restricted_background_in_call_desc">Приложението може да се затвори след 1 минута във фонов режим.</string>
|
||||
@@ -1506,7 +1449,6 @@
|
||||
<string name="verify_connection">Потвърди връзка</string>
|
||||
<string name="no_connected_mobile">Няма свързано мобилно устройство</string>
|
||||
<string name="blocked_item_description">блокиран</string>
|
||||
<string name="you_can_make_address_visible_via_settings">Можете да го направите видим за вашите контакти в SimpleX чрез Настройки.</string>
|
||||
<string name="recent_history_is_not_sent_to_new_members">Историята не се изпраща на нови членове.</string>
|
||||
<string name="retry_verb">Опитай отново</string>
|
||||
<string name="camera_not_available">Камерата е неодстъпна</string>
|
||||
@@ -1780,7 +1722,6 @@
|
||||
<string name="forward_alert_forward_messages_without_files">Препращане на съобщенията без файловете?</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Неизвестни сървъри</string>
|
||||
<string name="settings_section_title_files">Файлове</string>
|
||||
<string name="chat_list_always_visible">Показване на списъка на чатовете в нов прозорец</string>
|
||||
<string name="color_mode_system">Системна</string>
|
||||
<string name="color_mode_dark">Тъмна</string>
|
||||
<string name="theme_black">Черна</string>
|
||||
@@ -1833,7 +1774,6 @@
|
||||
<string name="network_smp_proxy_mode_always">Винаги</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Не</string>
|
||||
<string name="network_smp_proxy_mode_never">Никога</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Режим за рутиране на съобщения</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Използвай поверително рутиране с неизвестни сървъри, когато IP адресът не е защитен.</string>
|
||||
<string name="color_mode_light">Светла</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Да</string>
|
||||
@@ -1858,14 +1798,12 @@
|
||||
<string name="servers_info_reconnect_server_message">Свържете отново сървъра, за да принудите доставката на съобщенията. Това използва допълнителен трафик.</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Грешка при нулиране на статистиката</string>
|
||||
<string name="scan_paste_link">Сканирай / Постави линк</string>
|
||||
<string name="xftp_servers_configured">Конфигурирани XFTP сървъри</string>
|
||||
<string name="appearance_font_size">Размер на шрифта</string>
|
||||
<string name="share_text_message_status">Статус на съобщението: %s</string>
|
||||
<string name="chat_theme_set_default_theme">Задай тема по подразбиране</string>
|
||||
<string name="v5_8_safe_files_descr">Потвърди файлове от неизвестни сървъри.</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Изпратени съобщения</string>
|
||||
<string name="error_initializing_web_view">Грешка при стартиране на WebView. Актуализирайте системата си до новата версия. Моля, свържете се с разработчиците.\nГрешка: %s</string>
|
||||
<string name="xftp_servers_other">Други XFTP сървъри</string>
|
||||
<string name="appearance_zoom">Мащабиране</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Всички цветови режими</string>
|
||||
<string name="chat_theme_apply_to_light_mode">Светъл режим</string>
|
||||
@@ -1939,8 +1877,6 @@
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Получени грешки</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Моля, проверете дали мобилното и настолното устройство са свързани към една и съща локална мрежа и дали защитната стена на настолното устройство позволява връзката.\nМоля, споделете всички други проблеми с разработчиците.</string>
|
||||
<string name="temporary_file_error">Временна файлова грешка</string>
|
||||
<string name="smp_servers_configured">Конфигурирани SMP сървъри</string>
|
||||
<string name="smp_servers_other">Други SMP сървъри</string>
|
||||
<string name="v5_8_message_delivery">Подобрена доставка на съобщения</string>
|
||||
<string name="attempts_label">опити</string>
|
||||
<string name="expired_label">изтекли</string>
|
||||
@@ -2053,11 +1989,9 @@
|
||||
<string name="selected_chat_items_selected_n">Избрано %d</string>
|
||||
<string name="selected_chat_items_nothing_selected">Нищо не е избрано</string>
|
||||
<string name="compose_message_placeholder">Съобщение</string>
|
||||
<string name="info_view_open_button">отвори</string>
|
||||
<string name="info_view_message_button">съобщение</string>
|
||||
<string name="info_view_call_button">обаждане</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Контактът ще бъде изтрит - това не може да бъде отменено!</string>
|
||||
<string name="info_view_connect_button">свързване</string>
|
||||
<string name="delete_without_notification">Изтрий без известие</string>
|
||||
<string name="conversation_deleted">Разговорът е изтрит!</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Все още ще можете да видите разговора с %1$s в списъка с чатовете.</string>
|
||||
@@ -2073,7 +2007,6 @@
|
||||
<string name="contact_deleted">Контактът е изтрит!</string>
|
||||
<string name="deleted_chats">Архивирани контакти</string>
|
||||
<string name="one_hand_ui">Достъпни панели</string>
|
||||
<string name="open_server_settings_button">Отвори настройките на сървъра</string>
|
||||
<string name="chat_database_exported_continue">Продължи</string>
|
||||
<string name="app_check_for_updates_button_open">Отвори местоположението на файла</string>
|
||||
<string name="app_check_for_updates_button_install">Инсталиране на актуализация</string>
|
||||
@@ -2125,8 +2058,6 @@
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Например, ако контактът ви получава съобщения чрез SimpleX Chat сървър, приложението ви ще ги достави чрез Flux сървър.</string>
|
||||
<string name="onboarding_choose_server_operators">Сървърни оператори</string>
|
||||
<string name="onboarding_network_operators">Мрежови оператори</string>
|
||||
<string name="onboarding_network_operators_review_later">Преглед по-късно</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Можете да конфигурирате операторите в Мрежа и сървъри.</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Условията ще бъдат автоматично приети за активираните оператори на: %s.</string>
|
||||
<string name="your_servers">Вашите сървъри</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Условията ще бъдат приети на: %s.</string>
|
||||
@@ -2140,16 +2071,13 @@
|
||||
<string name="operator_added_message_servers">Добавени сървъри за съобщения</string>
|
||||
<string name="operator_use_for_files">Използвай за файлове</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- Отворете чата при първото непрочетено съобщение.\n- Преминете към цитирани съобщения.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Резервно рутиране на съобщения</string>
|
||||
<string name="debug_logs">Активирай логовете</string>
|
||||
<string name="add_your_team_members_to_conversations">Добавете членовете на вашия екип към разговорите.</string>
|
||||
<string name="strikethrough_text">задраскан</string>
|
||||
<string name="onboarding_network_operators_continue">Продължи</string>
|
||||
<string name="button_add_friends">Добави приятели</string>
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[ Условия са приети за оператора(ите): <b>%s</b>.]]></string>
|
||||
<string name="operator_website">Уеб страница</string>
|
||||
<string name="operator_conditions_accepted_on">Условия, приети на: %s.</string>
|
||||
<string name="remote_hosts_section">Дистанционни мобилни устройства</string>
|
||||
<string name="network_preset_servers_title">Предварително зададени сървъри</string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Ще бъдат приети условия за оператора(ите): <b>%s</b>.]]></string>
|
||||
<string name="operator_open_changes">Отвори промените</string>
|
||||
@@ -2160,7 +2088,6 @@
|
||||
<string name="v6_1_customizable_message_descr">Персонализирана форма на съобщенията.</string>
|
||||
<string name="v6_1_message_dates_descr">По-добри дати на съобщението.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Активиране на Flux в настройките Мрежа и сървъри за по -добра поверителност на метаданните.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">за по-добра поверителност на метаданните.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Подобрена навигация в чата</string>
|
||||
<string name="error_saving_database">Грешка при запазване на базата данни</string>
|
||||
<string name="chat_main_profile_sent">Вашият профил ще бъде изпратен до членовете</string>
|
||||
@@ -2174,14 +2101,11 @@
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi устройства </b>: моля, активирайте Autostart в системните настройки, за да работят известията.]]></string>
|
||||
<string name="message_deleted_or_not_received_error_title">Няма съобщение</string>
|
||||
<string name="or_to_share_privately">Или сподели лично</string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Всички съобщения и файлове се изпращат <b>криптирани от край до край</b>, с постквантова сигурност в директните съобщения.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc_short">Без фонова услуга</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc_short">Проверявай за съобщения на всеки 10 минути</string>
|
||||
<string name="onboarding_notifications_mode_service_desc_short">Приложението винаги работи във фонов режим</string>
|
||||
<string name="onboarding_notifications_mode_battery">Известия и батерия</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Условията ще бъдат приети за активираните оператори след 30 дни.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Можете да конфигурирате сървърите през настройките.</string>
|
||||
<string name="onboarding_network_operators_update">Актуализация</string>
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Ще се приемат условия за оператора(ите): <b>%s</b>.]]></string>
|
||||
<string name="operator_use_for_messages">Използвай за съобщения</string>
|
||||
<string name="direct_messages_are_prohibited">Личните съобщения между членовете са забранени.</string>
|
||||
@@ -2195,7 +2119,6 @@
|
||||
<string name="error_loading_chat_tags">Грешка при зареждането на чат списъка</string>
|
||||
<string name="no_chats_in_list">Няма чатове в списъка %s.</string>
|
||||
<string name="no_unread_chats">Няма непрочетени чатове</string>
|
||||
<string name="address_creation_instruction">Докосни Създаване на SimpleX адрес в менюто, за да го създадете по-късно.</string>
|
||||
<string name="no_chats">Няма чатове</string>
|
||||
<string name="no_chats_found">Няма намерени чатове</string>
|
||||
<string name="chat_list_favorites">Любими</string>
|
||||
@@ -2267,7 +2190,6 @@
|
||||
<string name="sync_connection_desc">Връзката изисква предоговаряне на криптирането.</string>
|
||||
<string name="share_1_time_link_with_a_friend">Сподели еднократен линк за връзка с приятел</string>
|
||||
<string name="address_settings">Настройки на адреса</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Изберете мрежови оператори за използване.</string>
|
||||
<string name="how_it_helps_privacy">Как помага за поверителността</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Ще спрете да получавате съобщения от този чат. Историята на чата ще бъде запазена.</string>
|
||||
<string name="button_add_team_members">Добави членове на екипа</string>
|
||||
@@ -2455,7 +2377,6 @@
|
||||
<string name="compose_view_send_request">Изпрати заявка</string>
|
||||
<string name="compose_view_send_request_without_message">Изпрати заявка без съобщение</string>
|
||||
<string name="v6_4_support_chat_descr">Изпрати личната си обратна връзка до групи.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Изпратено до вашия контакт след осъществяване на връзка.</string>
|
||||
<string name="text_field_set_chat_placeholder">Задаване на име на чат…</string>
|
||||
<string name="set_member_admission">Задаване на достъп за членове</string>
|
||||
<string name="v6_3_set_message_expiration_in_chats">Задаване на срок на валидност на съобщенията в чатовете.</string>
|
||||
@@ -2474,14 +2395,12 @@
|
||||
<string name="chat_banner_send_request_to_connect">Докосни Свързване за изпращане на заявка</string>
|
||||
<string name="chat_banner_join_group">Докосни Присъединяване към групата</string>
|
||||
<string name="network_smp_web_port_section_title">TCP порт за съобщения</string>
|
||||
<string name="report_archive_alert_desc">Докладът ще бъде архивиран за вас.</string>
|
||||
<string name="the_sender_will_not_be_notified">Подателят НЯМА да бъде уведомен.</string>
|
||||
<string name="change_automatic_chat_deletion_message">Това действие не може да бъде отменено - съобщенията, изпратени и получени в този чат по-рано от избраното, ще бъдат изтрити.</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">Тази връзка изисква по-нова версия на приложението. Моля, актуализирайте приложението или помолете контакта да ви изпрати съвместим линк.</string>
|
||||
<string name="time_to_disappear_is_set_only_for_new_contacts">Времето за изчезване е зададено само за нови контакти.</string>
|
||||
<string name="context_user_picker_cant_change_profile_alert_message">За да използвате друг профил след опит за връзка, изтрийте чата и използвайте линка отново.</string>
|
||||
<string name="unblock_members_for_all_question">Отблокиране за всички членове?</string>
|
||||
<string name="unread_mentions">Непрочетени споменавания</string>
|
||||
<string name="unsupported_connection_link">Неподдържан линк за връзка</string>
|
||||
<string name="operator_updated_conditions">Актуализирани условия</string>
|
||||
<string name="v6_4_1_short_address_update">Актуализирайте адреса си</string>
|
||||
@@ -2508,7 +2427,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">Заявки за контакт от групи</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Членът е изтрит - не може да се приеме заявката</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">заявка за връзка от група %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Тази настройка е за текущия профил</string>
|
||||
<string name="allow_files_and_media_only_if">Разреши файлове и медия само ако вашият контакт ги разрешава.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Позволи на вашите контактите да изпращат файлове и медия.</string>
|
||||
<string name="chat_banner_bot">Бот</string>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="accept_contact_button">গ্রহণ করুন</string>
|
||||
<string name="accept_connection_request__question">সংযোগের অনুরোধটি গ্রহণ করবেন কি\?</string>
|
||||
<string name="callstatus_accepted">কলটি গৃহীত হয়েছে</string>
|
||||
<string name="smp_servers_preset_add">পূর্বনির্ধারিত সার্ভারগুলি যুক্ত করুন</string>
|
||||
<string name="group_member_role_admin">অ্যাডমিন</string>
|
||||
<string name="connect_plan_you_are_admin">আপনি একজন অ্যাডমিন</string>
|
||||
<string name="button_add_welcome_message">স্বাগত বার্তা যুক্ত করুন</string>
|
||||
|
||||
@@ -69,8 +69,6 @@
|
||||
<string name="icon_descr_cancel_link_preview">cancel·la la vista prèvia de l\'enllaç</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Es compartirà un nou perfil aleatori.</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[l\'enllaç d\'un cop es pot utilitzar <i>només amb un contacte</i>: compartiu-lo en persona o mitjançant qualsevol missatger.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Si no us podeu trobar en persona, podeu <b>escanejar el codi QR a la videotrucada</b> o el vostre contacte pot compartir un enllaç d\'invitació.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Si no pots trobar-te en persona, <b>mostra el codi QR a la videotrucada</b> o comparteix l\'enllaç.]]></string>
|
||||
<string name="deleted_chats">Contactes arxivats</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Llegir més a la <font color="#0088ff">Guia de l\'usuari</font>.]]></string>
|
||||
<string name="network_settings">Configuració de xarxa avançada</string>
|
||||
@@ -98,12 +96,9 @@
|
||||
<string name="callstatus_in_progress">trucada en curs</string>
|
||||
<string name="permissions_camera">Càmera</string>
|
||||
<string name="permissions_camera_and_record_audio">Càmera i micròfon</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Qualsevol pot allotjar servidors.</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Tots els missatges i fitxers s\'envien <b>xifrats d\'extrem a extrem</b>, amb seguretat postquàntica als missatges directes.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>El millor per a la bateria</b>. Només rebràs notificacions quan l\'aplicació s\'està executant (Sense servei en segon pla).]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bo per a la bateria</b>. L\'aplicació revisa els missatges cada 10 minuts. Podeu perdre trucades o missatges urgents.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Llegir més al nostre <font color="#0088ff">repositori GitHub</font>.]]></string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s vol connectar amb tu mitjançant</string>
|
||||
<string name="always_use_relay">Feu servir sempre el retransmisor</string>
|
||||
<string name="onboarding_notifications_mode_service_desc_short">App always runs in background</string>
|
||||
@@ -171,7 +166,6 @@
|
||||
<string name="appearance_app_toolbars">Barres d\'eines d\'aplicació</string>
|
||||
<string name="appearance_bars_blur_radius">Desenfocar</string>
|
||||
<string name="audio_video_calls">Trucades d\'àudio/vídeo</string>
|
||||
<string name="available_in_v51">\nDisponible en v5.1</string>
|
||||
<string name="allow_calls_only_if">Permet trucades només si el vostre contacte ho permet.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Permet la desaparició de missatges només si el vostre contacte ho permet.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">"Permet la supressió irreversible del missatge només si el teu contacte t\'ho permet. (24 hores)"</string>
|
||||
@@ -237,7 +231,6 @@
|
||||
<string name="server_connecting">s\'està connectant</string>
|
||||
<string name="blocked_item_description">blocat</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_link_verb">Connecta</string>
|
||||
<string name="server_error">error</string>
|
||||
<string name="sender_you_pronoun">vostè</string>
|
||||
<string name="live">EN DIRECTE</string>
|
||||
@@ -291,7 +284,6 @@
|
||||
<string name="one_time_link_short">Enllaç d\'un sol ús</string>
|
||||
<string name="add_contact_tab">Afegiu un contacte</string>
|
||||
<string name="address_or_1_time_link">Adreça o enllaç d\'un sol ús?</string>
|
||||
<string name="smp_servers_preset_add">Afegiu servidors predefinits</string>
|
||||
<string name="smp_servers_add">Afegeix servidor</string>
|
||||
<string name="about_simplex">Quant a SimpleX</string>
|
||||
<string name="callstatus_accepted">trucada acceptada</string>
|
||||
@@ -374,7 +366,6 @@
|
||||
<string name="delete_address">Esborrar adreça</string>
|
||||
<string name="delete_image">Suprimeix la imatge</string>
|
||||
<string name="display_name_cannot_contain_whitespace">El nom mostrat no pot contenir espais en blanc.</string>
|
||||
<string name="decentralized">Descentralitzada</string>
|
||||
<string name="no_call_on_lock_screen">Desactivar</string>
|
||||
<string name="receipts_contacts_disable_for_all">Desactivar per a tothom</string>
|
||||
<string name="receipts_groups_disable_for_all">Desactiva per a tots els grups</string>
|
||||
@@ -485,7 +476,6 @@
|
||||
<string name="deleted">Suprimides</string>
|
||||
<string name="deletion_errors">Errors d\'eliminació</string>
|
||||
<string name="connected_mobile">Mòbil connectat</string>
|
||||
<string name="connect_via_link_incognito">Connectar en mode incògnit</string>
|
||||
<string name="connection_local_display_name">connexió %1$d</string>
|
||||
<string name="notifications_mode_periodic_desc">Comprova si hi ha missatges cada 10 minuts durant un minut com a màxim</string>
|
||||
<string name="la_change_app_passcode">Canvia el codi d\'accés</string>
|
||||
@@ -493,7 +483,6 @@
|
||||
<string name="auth_confirm_credential">Confirmeu les credencials</string>
|
||||
<string name="your_chats">Xats</string>
|
||||
<string name="chat_with_developers">Parleu amb els desenvolupadors</string>
|
||||
<string name="info_view_connect_button">connecta</string>
|
||||
<string name="confirm_delete_contact_question">Confirmeu l\'eliminació del contacte?</string>
|
||||
<string name="icon_descr_server_status_connected">Connectat</string>
|
||||
<string name="confirm_verb">Confirma</string>
|
||||
@@ -502,7 +491,6 @@
|
||||
<string name="connect_button">Connecta</string>
|
||||
<string name="chat_console">Consola del xat</string>
|
||||
<string name="smp_servers_check_address">Comproveu l\'adreça del servidor de i torneu a provar.</string>
|
||||
<string name="smp_servers_configured">Servidors SMP configurats</string>
|
||||
<string name="configure_ICE_servers">Configura els servidors ICE</string>
|
||||
<string name="network_session_mode_user">Perfil del xat</string>
|
||||
<string name="network_session_mode_entity">Connexió</string>
|
||||
@@ -548,7 +536,6 @@
|
||||
<string name="error_saving_file">Error en desar el fitxer</string>
|
||||
<string name="switch_receiving_address_question">Canviar l\'adreça de recepció?</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Podeu <font color="#0088ff">connectar amb els desenvolupadors de SimpleX Chat per fer qualsevol pregunta i rebre actualitzacions</font>.]]></string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[També us podeu connectar fent clic a l\'enllaç. Si s\'obre al navegador, feu clic al botó <b>Obre a l\'aplicació mòbil</b>.]]></string>
|
||||
<string name="error_saving_ICE_servers">Error en desar els servidors ICE</string>
|
||||
<string name="network_proxy_incorrect_config_title">Error en desar el servidor intermediari</string>
|
||||
<string name="chat_database_section">Base de dades dels xats</string>
|
||||
@@ -606,8 +593,6 @@
|
||||
<string name="enter_password_to_show">Introduïu la contrasenya</string>
|
||||
<string name="error_showing_content">error en mostrar el contingut</string>
|
||||
<string name="error_showing_message">error en mostrar el missatge</string>
|
||||
<string name="error_saving_smp_servers">Error en desar els servidors SMP</string>
|
||||
<string name="error_saving_xftp_servers">Error en desar els servidors XFTP</string>
|
||||
<string name="error_sending_message">Error en enviar el missatge</string>
|
||||
<string name="errors_in_servers_configuration">Errors en la configuració dels servidors.</string>
|
||||
<string name="failed_to_active_user_title">Error en canviar el perfil!</string>
|
||||
@@ -659,8 +644,6 @@
|
||||
<string name="error_creating_message">Error en crear el missatge</string>
|
||||
<string name="failed_to_create_user_title">Error en crear el perfil!</string>
|
||||
<string name="error_forwarding_messages">Error en reenviar els missatges</string>
|
||||
<string name="error_loading_smp_servers">Error en carregar els servidors SMP</string>
|
||||
<string name="error_loading_xftp_servers">Error en carregar els servidors XFTP</string>
|
||||
<string name="error_adding_members">Error en afegir membre(s)</string>
|
||||
<string name="error_creating_address">Error en crear l\'adreça</string>
|
||||
<string name="error_joining_group">Error en unir-se al grup</string>
|
||||
@@ -701,8 +684,6 @@
|
||||
<string name="v5_4_incognito_groups_descr">Crea un grup tot fent servir un perfil aleatori.</string>
|
||||
<string name="current_user">Perfil actual</string>
|
||||
<string name="create_1_time_link">Crea un enllaç d\'un sol ús</string>
|
||||
<string name="dont_create_address">No crear cap adreça</string>
|
||||
<string name="create_profile_button">Crea</string>
|
||||
<string name="create_another_profile_button">Crea perfil</string>
|
||||
<string name="settings_message_shape_corner">Cantonada</string>
|
||||
<string name="current_passphrase">Frase de pas actual…</string>
|
||||
@@ -720,11 +701,9 @@
|
||||
<string name="smp_server_test_create_file">Crea un fitxer</string>
|
||||
<string name="la_current_app_passcode">Codi d\'accés actual</string>
|
||||
<string name="maximum_supported_file_size">Ara per ara, la mida màxima per als fitxers és %1$s.</string>
|
||||
<string name="share_one_time_link">Crea un enllaç d\'invitació d\'un sol ús</string>
|
||||
<string name="create_group">Crea un grupo secreto</string>
|
||||
<string name="send_disappearing_message_custom_time">Temps personalitzat</string>
|
||||
<string name="create_chat_profile">Crea un perfil de xat</string>
|
||||
<string name="create_address_and_let_people_connect">Creeu una adreça perquè la gent pugui connectar amb vós.</string>
|
||||
<string name="create_simplex_address">Crea una adreça SimpleX</string>
|
||||
<string name="create_profile">Crea perfil</string>
|
||||
<string name="group_member_status_creator">creador</string>
|
||||
@@ -740,13 +719,11 @@
|
||||
<string name="toolbar_settings">Configuració</string>
|
||||
<string name="settings_shutdown">Tanca</string>
|
||||
<string name="contact_connection_pending">s\'està connectant…</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Les condicions s\'acceptaran per als operadors habilitats després de 30 dies.</string>
|
||||
<string name="system_restricted_background_desc">SimpleX no pot funcionar en segon pla. Només rebreu les notificacions quan obriu l\'aplicació.</string>
|
||||
<string name="ntf_channel_calls">Trucades de SimpleX Chat</string>
|
||||
<string name="ntf_channel_messages">Missatges de xat de SimpleX</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">enviat</string>
|
||||
<string name="icon_descr_received_msg_status_unread">per llegir</string>
|
||||
<string name="welcome">Benvinguts!</string>
|
||||
<string name="send_disappearing_message_send">Envia</string>
|
||||
<string name="send_verb">Envia</string>
|
||||
<string name="reset_verb">Restableix</string>
|
||||
@@ -826,7 +803,6 @@
|
||||
<string name="keep_conversation">Conserva la conversa</string>
|
||||
<string name="mark_unread">Marcar com ha llegit</string>
|
||||
<string name="invalid_contact_link">Enllaç no vàlid</string>
|
||||
<string name="invalid_QR_code">El codi QR no és vàlid</string>
|
||||
<string name="learn_more">Més informació</string>
|
||||
<string name="invalid_qr_code">El codi QR no és vàlid</string>
|
||||
<string name="incorrect_code">Codi de seguretat incorrecte!</string>
|
||||
@@ -913,11 +889,9 @@
|
||||
<string name="connect_with_contact_name_question">Connectar amb %1$s?</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Descarregar</string>
|
||||
<string name="sync_connection_desc">La connexió requereix una renegociació del xifratge.</string>
|
||||
<string name="connect_via_link_or_qr">Connectar mitjançant enllaç/codi QR</string>
|
||||
<string name="enable_camera_access">Habilita l\'accés a la càmera</string>
|
||||
<string name="connection_security">Seguretat de connexió</string>
|
||||
<string name="creating_link">Creant enllaç…</string>
|
||||
<string name="xftp_servers_configured">Servidors XFTP configurats</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">No utilitzeu credencials amb servidor intermediari.</string>
|
||||
<string name="core_version">Versió del nucli: v%s</string>
|
||||
<string name="customize_theme_title">Personalitzar tema</string>
|
||||
@@ -1006,7 +980,6 @@
|
||||
<string name="servers_info_files_tab">Fitxers</string>
|
||||
<string name="fix_connection_not_supported_by_contact">Correcció no suportada pel contacte</string>
|
||||
<string name="fix_connection_confirm">Arreglar</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">per a una millor privadesa de les metadades.</string>
|
||||
<string name="wallpaper_preview_hello_alice">Bona tarda!</string>
|
||||
<string name="migrate_from_device_exported_file_doesnt_exist">El fitxer exportat no existeix</string>
|
||||
<string name="v5_7_forward">Reenvia i desa missatges</string>
|
||||
@@ -1090,7 +1063,6 @@
|
||||
<string name="permissions_record_audio">Micròfon</string>
|
||||
<string name="icon_descr_call_missed">Trucada perduda</string>
|
||||
<string name="privacy_message_draft">Esborrany</string>
|
||||
<string name="remote_hosts_section">Mòbils remots</string>
|
||||
<string name="remove_passphrase">Elimina</string>
|
||||
<string name="remove_member_confirmation">Expulsa</string>
|
||||
<string name="rcv_group_event_user_deleted">t\'ha expulsat</string>
|
||||
@@ -1161,7 +1133,6 @@
|
||||
<string name="v5_8_chat_themes_descr">Canvia l\'aspecte dels teus xats!</string>
|
||||
<string name="v4_3_voice_messages_desc">Màxim 40 segons, rebut a l\'instant.</string>
|
||||
<string name="only_you_can_delete_messages">Només vos podreu suprimir els missatges de manera irreversible (el vostre contacte pot marcar-los per suprimir-los). (24 hores)</string>
|
||||
<string name="open_server_settings_button">Obrir configuració del servidor</string>
|
||||
<string name="other_errors">altres errors</string>
|
||||
<string name="v5_4_more_things_descr">- Notificació opcional als contactes suprimits.\n- Noms de perfil amb espais.\n- I més!</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Pendents</string>
|
||||
@@ -1188,8 +1159,6 @@
|
||||
<string name="save_auto_accept_settings">Desar la configuració d\'adreça SimpleX</string>
|
||||
<string name="stop_sharing">Deixar de compartir</string>
|
||||
<string name="or_to_share_privately">O per compartir en privat</string>
|
||||
<string name="you_can_create_it_later">Podeu crear-la més tard</string>
|
||||
<string name="you_can_make_address_visible_via_settings">Podeu fer-lo visible per als vostres contactes de SimpleX mitjançant Configuració.</string>
|
||||
<string name="display_name__field">Nom del perfil:</string>
|
||||
<string name="save_and_notify_contact">Desa i notifica el contacte</string>
|
||||
<string name="your_current_profile">El teu perfil actual</string>
|
||||
@@ -1200,7 +1169,6 @@
|
||||
<string name="password_to_show">Contrasenya per mostrar</string>
|
||||
<string name="save_profile_password">Desa la contrasenya del perfil</string>
|
||||
<string name="to_reveal_profile_enter_password">Per revelar el vostre perfil ocult introduïu una contrasenya completa al camp de cerca de la pàgina Els vostres perfils de xat.</string>
|
||||
<string name="you_control_your_chat">Tu controles el teu xat!</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Pots utilitzar la sintaxi markdown per donar format als teus missatges:</string>
|
||||
<string name="how_to_use_markdown">Com utilitzar la sintaxis markdown</string>
|
||||
<string name="callstatus_rejected">trucada rebutjada</string>
|
||||
@@ -1210,13 +1178,6 @@
|
||||
<string name="callstate_waiting_for_confirmation">esperant confirmació…</string>
|
||||
<string name="callstate_received_confirmation">confirmació rebuda…</string>
|
||||
<string name="unable_to_open_browser_desc">Un navegador web predeterminat és necessari per a les trucades. Configura\'n un al sistema i comparteix més informació amb els desenvolupadors.</string>
|
||||
<string name="privacy_redefined">Privadesa redefinida</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Tu decideixes qui es pot connectar.</string>
|
||||
<string name="immune_to_spam_and_abuse">Immune al correu brossa</string>
|
||||
<string name="first_platform_without_user_ids">Sense identificadors d\'usuari.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Per protegir la vostra privadesa SimpleX utilitza identificadors separats per a cadascun dels vostres contactes..</string>
|
||||
<string name="use_chat">Obrir SimpleX</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Només els dispositius client emmagatzemen perfils d\'usuari, contactes, grups i missatges.</string>
|
||||
<string name="onboarding_notifications_mode_title">Notificacions privades</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Com afecta la bateria</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periòdic</string>
|
||||
@@ -1226,9 +1187,6 @@
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">L\'aplicació protegeix la teva privadesa utilitzant diferents operadors en cada conversa.</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quan hi ha més d\'un operador habilitat, cap d\'ells té metadades per saber qui es comunica amb qui.</string>
|
||||
<string name="how_it_helps_privacy">Com ajuda a la privadesa</string>
|
||||
<string name="onboarding_network_operators_review_later">Revisar més tard</string>
|
||||
<string name="onboarding_network_operators_update">Actualitzar</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Podeu configurar els operadors a la configuració: Xarxa i Servidors.</string>
|
||||
<string name="video_call_no_encryption">videotrucada (sense xifrar)</string>
|
||||
<string name="ignore">Ignorar</string>
|
||||
<string name="reject">Rebutjar</string>
|
||||
@@ -1269,7 +1227,6 @@
|
||||
<string name="settings_section_title_delivery_receipts">Enviar els rebus de lliurament a</string>
|
||||
<string name="receipts_groups_override_disabled">L\'enviament de rebuts està desactivat per a %d grups</string>
|
||||
<string name="settings_restart_app">Reiniciar</string>
|
||||
<string name="settings_section_title_socks">Servidor intermediari SOCKS</string>
|
||||
<string name="settings_section_title_profile_images">Imatges de perfil</string>
|
||||
<string name="settings_section_title_themes">Temes</string>
|
||||
<string name="settings_message_shape_tail">Cua</string>
|
||||
@@ -1307,7 +1264,6 @@
|
||||
<string name="member_role_will_be_changed_with_invitation">El rol es canviarà a %s. El membre rebrà una nova invitació.</string>
|
||||
<string name="network_status">Estat de la xarxa</string>
|
||||
<string name="operator">Operador</string>
|
||||
<string name="operator_website">Lloc web</string>
|
||||
<string name="operator_use_for_sending">Per a enviar</string>
|
||||
<string name="network_options_reset_to_defaults">Restablir valors predeterminats</string>
|
||||
<string name="network_option_ping_count">Compte PING</string>
|
||||
@@ -1587,8 +1543,6 @@
|
||||
<string name="description_via_one_time_link">mitjançant enllaç d\'un sol ús</string>
|
||||
<string name="description_you_shared_one_time_link">has compartit un enllaç d\'un sol ús</string>
|
||||
<string name="failed_to_create_user_invalid_title">Nom mostrat no vàlid!</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assegureu-vos que les adreces del servidor SMP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades.</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assegureu-vos que les adreces del servidor XFTP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades.</string>
|
||||
<string name="no_media_servers_configured">No hi ha servidors multimèdia ni de fitxers.</string>
|
||||
<string name="no_message_servers_configured">No hi ha servidors de missatges.</string>
|
||||
<string name="no_message_servers_configured_for_private_routing">No hi ha servidors per a l\'encaminament de missatges privats.</string>
|
||||
@@ -1689,11 +1643,9 @@
|
||||
<string name="message_forwarded_desc">Encara no hi ha connexió directa, el missatge el reenvia l\'administrador.</string>
|
||||
<string name="revoke_file__action">Revocar fitxer</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">enviament no autoritzat</string>
|
||||
<string name="this_text_is_available_in_settings">Aquest text està disponible a la configuració</string>
|
||||
<string name="personal_welcome">Benvingut %1$s!</string>
|
||||
<string name="group_preview_join_as">Entrar com a %s</string>
|
||||
<string name="member_contact_send_direct_message">enviar per connectar</string>
|
||||
<string name="tap_to_start_new_chat">Toca per iniciar un xat nou</string>
|
||||
<string name="loading_chats">Carregant xats…</string>
|
||||
<string name="no_filtered_chats">No hi ha xats filtrats</string>
|
||||
<string name="search_or_paste_simplex_link">Cerqueu o enganxeu l\'enllaç SimpleX</string>
|
||||
@@ -1746,7 +1698,6 @@
|
||||
<string name="contact_sent_large_file">El vostre contacte ha enviat un fitxer més gran que la mida màxima admesa actualment (%1$s).</string>
|
||||
<string name="loading_remote_file_title">Carregant el fitxer</string>
|
||||
<string name="notifications">Notificacions</string>
|
||||
<string name="info_view_open_button">obrir</string>
|
||||
<string name="loading_remote_file_desc">Si us plau, espereu mentre es carrega el fitxer des del mòbil enllaçat</string>
|
||||
<string name="temporary_file_error">Error de fitxer temporal</string>
|
||||
<string name="voice_message">Missatge de veu</string>
|
||||
@@ -1754,7 +1705,6 @@
|
||||
<string name="voice_message_with_duration">Missatge de veu (%1$s )</string>
|
||||
<string name="waiting_for_file">Esperant el fitxer</string>
|
||||
<string name="only_delete_conversation">Només suprimeix la conversa</string>
|
||||
<string name="icon_descr_server_status_pending">Pendente</string>
|
||||
<string name="switch_receiving_address_desc">L\'adreça de recepció es canviarà per un servidor diferent. El canvi d\'adreça es completarà quan el remitent estigui en línia.</string>
|
||||
<string name="text_field_set_contact_placeholder">Estableix el nom del contacte…</string>
|
||||
<string name="you_can_still_send_messages_to_contact">Pots enviar missatges a %1$s des dels contactes arxivats.</string>
|
||||
@@ -1778,7 +1728,6 @@
|
||||
<string name="send_disappearing_message">Envia un missatge que desapareix</string>
|
||||
<string name="send_live_message">Envia missatge en directe</string>
|
||||
<string name="tap_to_scan">Toqueu per escanejar</string>
|
||||
<string name="to_share_with_your_contact">(per compartir amb el teu contacte)</string>
|
||||
<string name="voice_messages_prohibited">Missatges de veu prohibits!</string>
|
||||
<string name="thank_you_for_installing_simplex">Gràcies per instal·lar SimpleX Xat!</string>
|
||||
<string name="to_start_a_new_chat_help_header">Per iniciar un xat nou</string>
|
||||
@@ -1800,22 +1749,16 @@
|
||||
<string name="you_invited_a_contact">Has convidat un contacte</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">El vostre contacte ha d\'estar en línia perquè la connexió es completi.\nPots cancel·lar aquesta connexió i eliminar el contacte (i provar-ho més tard amb un enllaç nou).</string>
|
||||
<string name="show_QR_code">Mostrar codi QR</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Aquest no és un enllaç de connexió vàlid!</string>
|
||||
<string name="this_QR_code_is_not_a_link">Aquest codi QR no és un enllaç!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Et connectaràs al grup quan el dispositiu de l\'amfitrió estigui en línia. Espereu o comproveu més tard!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Et connectaràs quan s\'accepti la teva sol·licitud de connexió, si us plau, espera o consulta més tard!</string>
|
||||
<string name="if_you_cant_meet_in_person">Si no pots trobar-te en persona, mostra el codi QR en una videotrucada o comparteix l\'enllaç.</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Enganxeu l\'enllaç que heu rebut per connectar amb el vostre contacte…</string>
|
||||
<string name="share_invitation_link">Compartir enllaç d\'un sol ús</string>
|
||||
<string name="share_1_time_link_with_a_friend">Compartir enllaç d\'un sol ús amb un amic</string>
|
||||
<string name="share_address_publicly">Compartir adreça públicament</string>
|
||||
<string name="share_simplex_address_on_social_media">Comparteix l\'adreça SimpleX a les xarxes socials.</string>
|
||||
<string name="scan_qr_to_connect_to_contact">Per connectar-se, el vostre contacte pot escanejar el codi QR o utilitzar l\'enllaç de l\'aplicació.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Per protegir-vos de la substitució del vostre enllaç, podeu comparar els codis de seguretat de contacte.</string>
|
||||
<string name="you_can_accept_or_reject_connection">Quan algú sol·liciti la connexió, pots acceptar-la o rebutjar-la.</string>
|
||||
<string name="you_can_set_connection_name_to_remember">Podeu definir el nom de la connexió per recordar amb qui s\'ha compartit l\'enllaç.</string>
|
||||
<string name="you_can_share_your_address">Pots compartir la teva adreça com a enllaç o codi QR; qualsevol es pot connectar amb tu.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">S\'enviarà el teu perfil de xat\nal teu contacte</string>
|
||||
<string name="connect__your_profile_will_be_shared">El teu perfil %1$s es compartirà.</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Et connectaràs quan el dispositiu del teu contacte estigui en línia, si us plau, espera o consulta més tard!</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Si més tard decideixes eliminar la teva adreça els contactes no es perdran.</string>
|
||||
@@ -1844,9 +1787,6 @@
|
||||
<string name="media_and_file_servers">Servidors de fitxers i mitjans</string>
|
||||
<string name="message_servers">Servidors de missatges</string>
|
||||
<string name="smp_servers_new_server">Nou servidor</string>
|
||||
<string name="smp_servers_other">Altres servidors SMP</string>
|
||||
<string name="xftp_servers_other">Altres servidors XFTP</string>
|
||||
<string name="smp_servers_preset_server">Servidor preestablert</string>
|
||||
<string name="smp_servers_preset_address">Adreça predeterminada del servidor</string>
|
||||
<string name="chat_with_the_founder">Preguntes i idees</string>
|
||||
<string name="send_us_an_email">Contacta via email</string>
|
||||
@@ -1861,7 +1801,6 @@
|
||||
<string name="smp_servers_use_server_for_new_conn">Utilitzar per a noves connexions</string>
|
||||
<string name="smp_servers_use_server">Utilitzar servidor</string>
|
||||
<string name="your_chat_profiles">Perfils de xat</string>
|
||||
<string name="smp_servers_your_server">El teu servidor</string>
|
||||
<string name="smp_servers_your_server_address">L\'adreça del teu servidor</string>
|
||||
<string name="your_settings">Configuració</string>
|
||||
<string name="your_simplex_contact_address">La teva adreça SimpleX</string>
|
||||
@@ -1878,8 +1817,6 @@
|
||||
<string name="use_simplex_chat_servers__question">Utilitzar servidors SimpleX Xat?</string>
|
||||
<string name="using_simplex_chat_servers">Usant servidors SimpleX Xat.</string>
|
||||
<string name="your_ICE_servers">Servidors ICE</string>
|
||||
<string name="your_SMP_servers">Servidors SMP</string>
|
||||
<string name="your_XFTP_servers">Servidors XFTP</string>
|
||||
<string name="network_and_servers">Xarxa i servidors</string>
|
||||
<string name="network_proxy_auth">Autenticació d\'intermediari</string>
|
||||
<string name="network_socks_proxy">Servidor intermediari SOCKS</string>
|
||||
@@ -1896,7 +1833,6 @@
|
||||
<string name="network_smp_proxy_mode_private_routing">Encaminament privat</string>
|
||||
<string name="network_session_mode_server">Servidor</string>
|
||||
<string name="network_session_mode_transport_isolation">Aïllament de transport</string>
|
||||
<string name="update_network_session_mode_question">Actualitzar el mode d\'aïllament de transport?</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Utilitza credencials de servidors intermediari diferents per a cada connexió.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Utilitza credencials de servidor intermediari diferents per a cada perfil.</string>
|
||||
<string name="network_disable_socks">Utilitzar connexió a Internet directa?</string>
|
||||
@@ -1905,8 +1841,6 @@
|
||||
<string name="network_use_onion_hosts_prefer">Si disponibles</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Les vostres credencials es podrien enviar sense xifrar.</string>
|
||||
<string name="theme_colors_section_title">Colors de la interfície</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Alternativa d\'encaminament de missatges</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mode d\'encaminament de missatges</string>
|
||||
<string name="app_check_for_updates_button_open">Obrir ubicació del fitxer</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Envieu missatges directament quan l\'adreça IP està protegida i el vostre servidor de destinació no admet l\'encaminament privat.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Enviar missatges directament quan el vostre servidor de destinació no admet l\'encaminament privat.</string>
|
||||
@@ -1927,23 +1861,18 @@
|
||||
<string name="show_slow_api_calls">Mostrar trucades lentes d\'API</string>
|
||||
<string name="app_check_for_updates_notice_desc">Per rebre notificacions sobre les noves versions activeu la comprovació periòdica de les versions Estable o Beta.</string>
|
||||
<string name="share_with_contacts">Compartir amb contactes</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de missatgeria i aplicacions que protegeix la vostra privadesa i seguretat.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">El perfil només es comparteix amb els teus contactes.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">No emmagatzemem cap dels vostres contactes o missatges (un cop lliurats) als servidors.</string>
|
||||
<string name="your_profile_is_stored_on_your_device">El vostre perfil, contactes i missatges lliurats s\'emmagatzemen al vostre dispositiu.</string>
|
||||
<string name="permissions_open_settings">Obrir configuració</string>
|
||||
<string name="next_generation_of_private_messaging">El futur de la missatgeria</string>
|
||||
<string name="onboarding_network_operators">Operadors de xarxa</string>
|
||||
<string name="onboarding_notifications_mode_battery">Notificacions i bateria</string>
|
||||
<string name="you_can_change_it_later">La contrasenya aleatòria s\'emmagatzema a la configuració com a text pla.\nPodeu canviar-ho més tard.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Seleccioneu els operadors de xarxa que voleu utilitzar.</string>
|
||||
<string name="onboarding_choose_server_operators">Operadors de servidor</string>
|
||||
<string name="setup_database_passphrase">Configura la contrasenya de la base de dades</string>
|
||||
<string name="use_random_passphrase">Utilitzar contrasenya aleatòria</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Obrir SimpleX Chat per acceptar la trucada</string>
|
||||
<string name="relay_server_protects_ip">El servidor de retransmissió protegeix la vostra adreça IP, però pot veure la durada de la trucada.</string>
|
||||
<string name="webrtc_ice_servers">Servidors WebRTC ICE</string>
|
||||
<string name="your_ice_servers">Servidors ICE</string>
|
||||
<string name="privacy_and_security">Privacitat i seguretat</string>
|
||||
<string name="protect_app_screen">Protegeix la pantalla de l\'aplicació</string>
|
||||
<string name="protect_ip_address">Protegir l\'adreça IP</string>
|
||||
@@ -1978,7 +1907,6 @@
|
||||
<string name="restore_passphrase_not_found_desc">No s\'ha trobat la contrasenya a Keystore, introduïu-la manualment. Això pot haver passat si vau restaurar les dades de l\'aplicació mitjançant una eina de còpia de seguretat. Si no és el cas, poseu-vos en contacte amb els desenvolupadors.</string>
|
||||
<string name="one_hand_ui">Barres d\'eines d\'aplicacions accessible</string>
|
||||
<string name="chat_bottom_bar">Barres d\'eines de xat accessible</string>
|
||||
<string name="chat_list_always_visible">Mostra la llista de xat en una finestra nova</string>
|
||||
<string name="terminal_always_visible">Mostrar consola en finestra nova</string>
|
||||
<string name="one_hand_ui_change_instruction">Podeu canviar-la a la configuració de l\'aparença.</string>
|
||||
<string name="upgrade_and_open_chat">Actualitzar i obrir el xat</string>
|
||||
@@ -2135,7 +2063,6 @@
|
||||
<string name="v5_5_new_interface_languages">IU en hongarès i turc</string>
|
||||
<string name="passcode_not_changed">Codi d\'accés no canviat!</string>
|
||||
<string name="v5_8_private_routing_descr">Protegiu la vostra adreça IP dels servidors de retransmissió de missatgeria escollits pels vostres contactes.\nActiva a la configuració de *Xarxa i servidors*.</string>
|
||||
<string name="address_creation_instruction">Toqueu Crea adreça SimpleX al menú per crear-la més tard.</string>
|
||||
<string name="v4_5_private_filenames_descr">Per protegir la zona horària els fitxers d\'imatge/veu utilitzen UTC.</string>
|
||||
<string name="unblock_member_confirmation">Desbloca</string>
|
||||
<string name="migrate_from_device_upload_failed">La càrrega ha fallat</string>
|
||||
@@ -2174,7 +2101,6 @@
|
||||
<string name="possible_slow_function_title">Funció lenta</string>
|
||||
<string name="group_preview_you_are_invited">Estat convidats(des) a un grup</string>
|
||||
<string name="info_view_search_button">cerca</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(escaneja o enganxa del porta-retalls)</string>
|
||||
<string name="scan_QR_code">Escaneja un codi QR</string>
|
||||
<string name="add_contact_or_create_group">Comença una conversa nova</string>
|
||||
<string name="image_descr_simplex_logo">Logo de SimpleX</string>
|
||||
@@ -2185,8 +2111,6 @@
|
||||
<string name="security_code">Codi de seguretat</string>
|
||||
<string name="smp_save_servers_question">Voleu desar els servidors?</string>
|
||||
<string name="smp_servers_scan_qr">Escaneja el codi QR del servidor</string>
|
||||
<string name="smp_servers">Servidors SMP</string>
|
||||
<string name="xftp_servers">Servidors XFTP</string>
|
||||
<string name="audio_device_speaker">Altaveu</string>
|
||||
<string name="settings_section_title_you">Vós</string>
|
||||
<string name="chat_item_ttl_seconds">%s segon(s)</string>
|
||||
@@ -2262,7 +2186,6 @@
|
||||
<string name="report_reason_illegal">Contingut inadequat</string>
|
||||
<string name="report_reason_profile">Perfil inadequat</string>
|
||||
<string name="delete_report">Suprimeix l\'informe</string>
|
||||
<string name="report_archive_alert_desc">L\'informe s\'arxivarà.</string>
|
||||
<string name="report_verb">Informar</string>
|
||||
<string name="blocking_reason_spam">Spam</string>
|
||||
<string name="blocking_reason_content">El contingut infringeix les condicions d\'ús</string>
|
||||
@@ -2300,7 +2223,6 @@
|
||||
<string name="network_smp_web_port_toggle">Emprar port web</string>
|
||||
<string name="network_smp_web_port_footer">Emprar el port TCP %1$s quan no se n\'especifica cap.</string>
|
||||
<string name="mute_all_chat">Silenciar tot</string>
|
||||
<string name="unread_mentions">Mencions no llegides</string>
|
||||
<string name="max_group_mentions_per_message_reached">Podeu mencionar fins a %1$s membres per missatge!</string>
|
||||
<string name="enable_sending_member_reports">Permetre denunciar missatges a moderació.</string>
|
||||
<string name="disable_sending_member_reports">No permetre denunciar missatges a moderació.</string>
|
||||
@@ -2427,7 +2349,6 @@
|
||||
<string name="compose_view_send_contact_request_alert_question">Enviar sol·licitud de contacte?</string>
|
||||
<string name="compose_view_send_request">Enviar sol·licitud</string>
|
||||
<string name="compose_view_send_request_without_message">Enviar sol·licitud sense missatge</string>
|
||||
<string name="sent_to_your_contact_after_connection">Enviat al contacte després de la connexió.</string>
|
||||
<string name="share_group_profile_via_link">Actualitzar l\'enllaç del grup?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Actualitzar</string>
|
||||
<string name="share_profile_via_link">Actualitzar l\'adreça?</string>
|
||||
@@ -2482,7 +2403,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">Sol·licituds de contacte de grups</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Membre eliminat(da); no es pot acceptar la sol·licitud.</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">connexió sol·licitada del grup %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Aquesta configuració és per al perfil actual</string>
|
||||
<string name="allow_files_and_media_only_if">Permetre fitxers i contingut multimèdia només si el vostre contacte ho permet.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Permetre que els contactes enviïn fitxers i contingut multimèdia.</string>
|
||||
<string name="chat_banner_bot">Bot</string>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<string name="allow_to_send_voice">Hlasové zprávy povoleny.</string>
|
||||
<string name="v4_2_group_links_desc">Správci mohou vytvářet odkazy pro připojení ke skupinám.</string>
|
||||
<string name="accept_contact_button">Přijmout</string>
|
||||
<string name="smp_servers_preset_add">Přidat přednastavené servery</string>
|
||||
<string name="network_settings">Pokročilá nastavení sítě</string>
|
||||
<string name="accept">Přijmout</string>
|
||||
<string name="smp_servers_add">Přidat server</string>
|
||||
@@ -115,7 +114,6 @@
|
||||
<string name="simplex_link_mode_full">Úplný odkaz</string>
|
||||
<string name="simplex_link_mode_browser">Prostřednictvím prohlížeče</string>
|
||||
<string name="simplex_link_mode_browser_warning">Otevření odkazu v prohlížeči může snížit soukromí a bezpečnost připojení. Nedůvěryhodné odkazy SimpleX budou červené.</string>
|
||||
<string name="error_saving_smp_servers">Chyba ukládání serverů SMP</string>
|
||||
<string name="error_setting_network_config">Chyba změny konfigurace sítě</string>
|
||||
<string name="failed_to_parse_chat_title">Nepodařilo se načíst chat</string>
|
||||
<string name="failed_to_parse_chats_title">Nepodařilo se načíst chaty</string>
|
||||
@@ -145,8 +143,6 @@
|
||||
<string name="copy_verb">Kopírovat</string>
|
||||
<string name="icon_descr_received_msg_status_unread">nepřečteno</string>
|
||||
<string name="personal_welcome">Vítejte %1$s!</string>
|
||||
<string name="welcome">Vítejte!</string>
|
||||
<string name="this_text_is_available_in_settings">Tento text je k dispozici v nastavení</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">odeslání se nezdařilo</string>
|
||||
<string name="share_file">Sdílet soubor…</string>
|
||||
<string name="attach">Připojit</string>
|
||||
@@ -179,7 +175,6 @@
|
||||
<string name="copied">Zkopírováno</string>
|
||||
<string name="add_contact_or_create_group">Nová konverzace</string>
|
||||
<string name="create_group">Vytvořit tajnou skupinu</string>
|
||||
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
|
||||
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
|
||||
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
|
||||
<string name="use_camera_button">Fotoaparát</string>
|
||||
@@ -210,7 +205,6 @@
|
||||
<string name="security_code">Bezpečnostní kód</string>
|
||||
<string name="is_verified">%s ověřen</string>
|
||||
<string name="chat_console">Chat konzole</string>
|
||||
<string name="smp_servers">SMP servery</string>
|
||||
<string name="smp_servers_preset_address">Přednastavená adresa serveru</string>
|
||||
<string name="smp_servers_test_failed">Test serveru se nezdařil!</string>
|
||||
<string name="smp_servers_test_some_failed">Některé servery neprošly testem:</string>
|
||||
@@ -234,15 +228,12 @@
|
||||
<string name="create_address">Vytvořit adresu</string>
|
||||
<string name="save_and_notify_group_members">Uložit a upozornit členy skupiny</string>
|
||||
<string name="exit_without_saving">Ukončit bez uložení</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string>
|
||||
<string name="create_profile">Vytvořit profil</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Profil je sdílen pouze s vašimi kontakty.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat bílé znaky.</string>
|
||||
<string name="bold_text">tučně</string>
|
||||
<string name="callstatus_in_progress">probíhající hovor</string>
|
||||
<string name="decentralized">Decentralizovaná</string>
|
||||
<string name="how_it_works">Jak to funguje</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy.</string>
|
||||
<string name="onboarding_notifications_mode_title">Soukromé oznámení</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Pravidelné</string>
|
||||
<string name="ignore">Ignorovat</string>
|
||||
@@ -264,7 +255,6 @@
|
||||
<string name="settings_section_title_device">Zařízení</string>
|
||||
<string name="settings_section_title_chats">Konverzace</string>
|
||||
<string name="settings_experimental_features">Experimentální funkce</string>
|
||||
<string name="settings_section_title_socks">SOCKS proxy</string>
|
||||
<string name="settings_section_title_icon">Ikona aplikace</string>
|
||||
<string name="settings_section_title_themes">Témata</string>
|
||||
<string name="settings_section_title_messages">Zprávy a soubory</string>
|
||||
@@ -335,15 +325,12 @@
|
||||
<string name="install_simplex_chat_for_terminal">Instalovat SimpleX Chat terminál</string>
|
||||
<string name="star_on_github">Hvězdu na GitHubu</string>
|
||||
<string name="rate_the_app">Ohodnoťte aplikaci</string>
|
||||
<string name="your_SMP_servers">Vaše servery SMP</string>
|
||||
<string name="network_disable_socks_info">Pokud potvrdíte, budou servery zasílající zprávy vidět vaši IP adresu a váš poskytovatel - ke kterým serverům se připojujete.</string>
|
||||
<string name="colored_text">barevné</string>
|
||||
<string name="secret_text">tajné</string>
|
||||
<string name="callstatus_calling">volání…</string>
|
||||
<string name="callstate_connected">připojen</string>
|
||||
<string name="callstate_ended">ukončen</string>
|
||||
<string name="next_generation_of_private_messaging">Budoucnost soukromé komunikace</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Rozhodněte, kdo se může připojit.</string>
|
||||
<string name="integrity_msg_bad_hash">špatný kontrolní součet zprávy</string>
|
||||
<string name="chat_database_imported">Databáze chatu importována</string>
|
||||
<string name="new_passphrase">Nová přístupová fráze…</string>
|
||||
@@ -361,7 +348,6 @@
|
||||
<string name="delete_pending_connection__question">Smazat čekající připojení\?</string>
|
||||
<string name="icon_descr_settings">Nastavení</string>
|
||||
<string name="image_descr_qr_code">QR kód</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Pokud se nemůžete setkat osobně, ukažte ve <b>videohovoru QR kód</b> nebo sdílejte odkaz.]]></string>
|
||||
<string name="scan_code">Skenovat kód</string>
|
||||
<string name="incorrect_code">Nesprávný bezpečnostní kód!</string>
|
||||
<string name="scan_code_from_contacts_app">Naskenujte bezpečnostní kód z aplikace vašeho kontaktu.</string>
|
||||
@@ -377,7 +363,6 @@
|
||||
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Pro připojení budou vyžadováni Onion hostitelé.
|
||||
\nVezměte prosím na vědomí: nebudete mít možnost připojení k serverům bez adresy .onion.</string>
|
||||
<string name="update_network_session_mode_question">Aktualizovat režim dopravní izolace\?</string>
|
||||
<string name="app_version_code">Sestavení aplikace: %s</string>
|
||||
<string name="share_link">Sdílet odkaz</string>
|
||||
<string name="delete_address">Smazat adresu</string>
|
||||
@@ -423,13 +408,10 @@
|
||||
<string name="voice_messages_prohibited">Hlasové zprávy jsou zakázány!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</string>
|
||||
<string name="send_live_message_desc">Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní.</string>
|
||||
<string name="share_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="scan_QR_code">Skenovat QR kód</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vložit ze schránky)</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<string name="delete_image">Smazat obrázek</string>
|
||||
<string name="callstatus_error">chyba volání</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Servery může provozovat kdokoli.</string>
|
||||
<string name="create_your_profile">Vytvořte si svůj profil</string>
|
||||
<string name="make_private_connection">Vytvořte si soukromé připojení</string>
|
||||
<string name="encrypted_video_call">Videohovor šifrovaný e2e</string>
|
||||
@@ -490,7 +472,6 @@
|
||||
<string name="v4_5_transport_isolation">Izolace přenosu</string>
|
||||
<string name="v4_5_transport_isolation_descr">Podle profilu chatu (výchozí) nebo podle připojení (BETA).</string>
|
||||
<string name="you_will_join_group">Připojtíte se ke všem členům skupiny.</string>
|
||||
<string name="connect_via_link_verb">Připojení</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string>
|
||||
<string name="error_connecting_to_server_to_receive_messages">Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s).</string>
|
||||
<string name="marked_deleted_description">označeno jako smazáno</string>
|
||||
@@ -501,7 +482,6 @@
|
||||
<string name="invalid_message_format">neplatný formát zprávy</string>
|
||||
<string name="live">ŽIVĚ</string>
|
||||
<string name="description_via_group_link_incognito">inkognito přes skupinový odkaz</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů SMP jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="failed_to_create_user_title">Chyba vytváření profilu!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicitní zobrazované jméno!</string>
|
||||
<string name="failed_to_active_user_title">Chyba přepínání profilu!</string>
|
||||
@@ -549,7 +529,6 @@
|
||||
<string name="group_preview_you_are_invited">Jste pozváni do skupiny</string>
|
||||
<string name="group_preview_join_as">Připojit jako %s</string>
|
||||
<string name="group_connection_pending">připojuji…</string>
|
||||
<string name="tap_to_start_new_chat">Začněte nový chat</string>
|
||||
<string name="chat_with_developers">Chat s vývojáři</string>
|
||||
<string name="you_have_no_chats">Nemáte žádné konverzace</string>
|
||||
<string name="icon_descr_cancel_image_preview">Zrušit náhled obrázku</string>
|
||||
@@ -560,13 +539,11 @@
|
||||
<string name="waiting_for_file">Čekání na soubor</string>
|
||||
<string name="notifications">Oznámení</string>
|
||||
<string name="delete_contact_question">Smazat kontakt\?</string>
|
||||
<string name="icon_descr_server_status_pending">Čeká na vyřízení</string>
|
||||
<string name="verify_security_code">Ověřit bezpečnostní kód</string>
|
||||
<string name="icon_descr_send_message">Odeslat zprávu</string>
|
||||
<string name="only_group_owners_can_enable_voice">Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</string>
|
||||
<string name="send_live_message">Odeslat živou zprávu</string>
|
||||
<string name="live_message">Živá zpráva!</string>
|
||||
<string name="connect_via_link_or_qr">Připojit se prostřednictvím odkazu / QR kódu</string>
|
||||
<string name="thank_you_for_installing_simplex">Děkujeme za instalaci SimpleX Chat!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Můžete se <font color="#0088ff">připojit k SimpleX Chat vývojářům, položit jim případné dotazy a získat aktualizace</font>.]]></string>
|
||||
<string name="to_connect_via_link_title">Připojení prostřednictvím odkazu</string>
|
||||
@@ -592,24 +569,17 @@
|
||||
<string name="icon_descr_email">E-mail</string>
|
||||
<string name="icon_descr_more_button">Více</string>
|
||||
<string name="show_QR_code">Zobrazit QR kód</string>
|
||||
<string name="invalid_QR_code">Neplatný QR kód</string>
|
||||
<string name="this_QR_code_is_not_a_link">Tento QR kód není odkaz!</string>
|
||||
<string name="invalid_contact_link">Neplatný odkaz!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Tento odkaz není platným odkazem pro připojení!</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Pokud se nemůžete setkat osobně, můžete <b>skenovat QR kód ve videohovoru</b> nebo může váš kontakt sdílet pozvánku.]]></string>
|
||||
<string name="connect_via_link">Připojte se prostřednictvím odkazu</string>
|
||||
<string name="connect_button">Připojit</string>
|
||||
<string name="paste_button">Vložit</string>
|
||||
<string name="this_string_is_not_a_connection_link">Tento řetězec není odkazem na připojení!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko <b>Otevřít v mobilní aplikaci</b>.]]></string>
|
||||
<string name="is_not_verified">%s neověřen</string>
|
||||
<string name="how_to_use_simplex_chat">Návod k použití</string>
|
||||
<string name="markdown_help">Nápověda k markdown</string>
|
||||
<string name="smp_servers_save">Uložit servery</string>
|
||||
<string name="markdown_in_messages">Markdown ve zprávách</string>
|
||||
<string name="smp_servers_test_servers">Testovací servery</string>
|
||||
<string name="smp_servers_preset_server">Přednastavený server</string>
|
||||
<string name="smp_servers_your_server">Váš server</string>
|
||||
<string name="smp_servers_your_server_address">Adresa vašeho serveru</string>
|
||||
<string name="smp_servers_use_server">Použít server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Použít pro nová připojení</string>
|
||||
@@ -640,11 +610,8 @@
|
||||
<string name="save_preferences_question">Uložit předvolby\?</string>
|
||||
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
|
||||
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
|
||||
<string name="you_control_your_chat">Kontrolujete konverzaci!</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukládáme žádné vaše kontakty ani zprávy (po doručení).</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</string>
|
||||
<string name="display_name">Zadejte vaše jméno:</string>
|
||||
<string name="create_profile_button">Vytvořit</string>
|
||||
<string name="how_to_use_markdown">Jak používat markdown</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">K formátování zpráv můžete použít markdown:</string>
|
||||
<string name="italic_text">kurzíva</string>
|
||||
@@ -659,12 +626,6 @@
|
||||
<string name="callstate_received_answer">obdržel odpověď…</string>
|
||||
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
|
||||
<string name="callstate_connecting">připojování…</string>
|
||||
<string name="privacy_redefined">Nové vymezení soukromí</string>
|
||||
<string name="first_platform_without_user_ids">Bez uživatelských identifikátorů</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, SimpleX používá ID pro každý z vašich kontaktů.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Další informace najdete v našem <font color="#0088ff">úložišti GitHub</font>.]]></string>
|
||||
<string name="use_chat">Použijte chat</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Jak ovlivňuje baterii</string>
|
||||
<string name="onboarding_notifications_mode_off">Když aplikace běží</string>
|
||||
<string name="onboarding_notifications_mode_service">Okamžité</string>
|
||||
@@ -682,7 +643,6 @@
|
||||
<string name="always_use_relay">Spojení přes relé</string>
|
||||
<string name="show_call_on_lock_screen">Zobrazit</string>
|
||||
<string name="no_call_on_lock_screen">Zakázat</string>
|
||||
<string name="your_ice_servers">Vaše servery ICE</string>
|
||||
<string name="webrtc_ice_servers">WebRTC servery ICE</string>
|
||||
<string name="relay_server_protects_ip">Přenosový server chrání vaši IP adresu, ale může sledovat dobu trvání hovoru.</string>
|
||||
<string name="relay_server_if_necessary">Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu.</string>
|
||||
@@ -921,7 +881,6 @@
|
||||
<string name="v4_5_italian_interface">Italské rozhraní</string>
|
||||
<string name="v4_5_italian_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, jakmile bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš profil chatu bude odeslán \nvašemu kontaktu</string>
|
||||
<string name="your_chats">Konverzace</string>
|
||||
<string name="share_invitation_link">Sdílet jednorázovou pozvánku</string>
|
||||
<string name="status_e2e_encrypted">koncově šifrované</string>
|
||||
@@ -1014,17 +973,12 @@
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Video bude přijato, až kontakt dokončí jeho nahrávání.</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později!</string>
|
||||
<string name="waiting_for_video">Čekám na video</string>
|
||||
<string name="error_loading_smp_servers">Chyba načítání serverů SMP</string>
|
||||
<string name="error_loading_xftp_servers">Chyba načítání serverů XFTP</string>
|
||||
<string name="error_saving_xftp_servers">Chyba ukládání XFTP serverů</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy XFTP serverů jsou ve správném formátu s oddělenými řádky a nejsou duplicitní.</string>
|
||||
<string name="error_xftp_test_server_auth">Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo.</string>
|
||||
<string name="smp_server_test_compare_file">Porovnat soubor</string>
|
||||
<string name="smp_server_test_create_file">Vytvořit soubor</string>
|
||||
<string name="smp_server_test_delete_file">Smazat soubor</string>
|
||||
<string name="smp_server_test_download_file">Stáhnout soubor</string>
|
||||
<string name="smp_server_test_upload_file">Nahrát soubor</string>
|
||||
<string name="xftp_servers">XFTP servery</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Použít SOCKS proxy</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
@@ -1047,7 +1001,6 @@
|
||||
<string name="you_can_turn_on_lock">Zámek SimpleX můžete zapnout v Nastavení.</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Nastavte <i>Použít .onion hostitele</i> na Ne, pokud je SOCKS proxy nepodporuje.]]></string>
|
||||
<string name="your_XFTP_servers">Vaše XFTP servery</string>
|
||||
<string name="enable_lock">Povolit zámek</string>
|
||||
<string name="lock_after">Zamknout po</string>
|
||||
<string name="lock_mode">Režim zámku</string>
|
||||
@@ -1077,8 +1030,6 @@
|
||||
<string name="allow_calls_only_if">Povolte hovory, pouze pokud je váš kontakt povolí.</string>
|
||||
<string name="allow_your_contacts_to_call">Povolte svým kontaktům vám volat.</string>
|
||||
<string name="audio_video_calls">Audio/video hovory</string>
|
||||
<string name="available_in_v51">"
|
||||
\nDostupné ve verzi 5.1"</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Volat můžete vy i váš kontakt.</string>
|
||||
<string name="only_you_can_make_calls">Volat můžete pouze vy.</string>
|
||||
<string name="prohibit_calls">Zákaz audio/video hovorů.</string>
|
||||
@@ -1165,7 +1116,6 @@
|
||||
<string name="opening_database">Otevírání databáze…</string>
|
||||
<string name="error_setting_address">Chyba nastavení adresy</string>
|
||||
<string name="scan_qr_to_connect_to_contact">Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci.</string>
|
||||
<string name="you_can_accept_or_reject_connection">Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.</string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Přečtěte si více v <font color="#0088ff">Uživatelské příručce</font>.]]></string>
|
||||
<string name="simplex_address">Adresa SimpleX</string>
|
||||
<string name="theme_colors_section_title">Barvy motivu</string>
|
||||
@@ -1173,12 +1123,9 @@
|
||||
<string name="profile_update_will_be_sent_to_contacts">Aktualizace profilu bude zaslána vašim kontaktům.</string>
|
||||
<string name="share_address_with_contacts_question">Sdílet adresu s kontakty?</string>
|
||||
<string name="stop_sharing_address">Přestat sdílet adresu\?</string>
|
||||
<string name="create_address_and_let_people_connect">Vytvořit adresu, aby se s vámi lidé mohli spojit.</string>
|
||||
<string name="save_auto_accept_settings">Uložit nastavení SimpleX adresy</string>
|
||||
<string name="stop_sharing">Přestat sdílet</string>
|
||||
<string name="auto_accept_contact">Automaticky přijmout</string>
|
||||
<string name="you_can_create_it_later">Můžete vytvořit později</string>
|
||||
<string name="dont_create_address">Nevytvářet adresu</string>
|
||||
<string name="email_invite_body">Ahoj!
|
||||
\nSpojte se se mnou přes SimpleX Chat: %s</string>
|
||||
<string name="email_invite_subject">Promluvme si v SimpleX Chatu</string>
|
||||
@@ -1224,7 +1171,6 @@
|
||||
<string name="only_your_contact_can_add_message_reactions">Reakce na zprávy může přidávat pouze váš kontakt.</string>
|
||||
<string name="your_contacts_will_remain_connected">Vaše kontakty zůstanou připojeny.</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">Tuto adresu můžete sdílet se svými kontakty, aby se mohli připojit k %s.</string>
|
||||
<string name="you_can_share_your_address">Svou adresu můžete sdílet jako odkaz nebo QR kód - kdokoli se k vám může připojit.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Pokud později adresu odstraníte, o kontakty nepřijdete.</string>
|
||||
<string name="app_passcode_replaced_with_self_destruct">Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</string>
|
||||
<string name="item_info_no_text">žádný text</string>
|
||||
@@ -1309,7 +1255,6 @@
|
||||
<string name="v5_2_message_delivery_receipts_descr">Druhé zaškrtnutí jsme přehlédli! ✅</string>
|
||||
<string name="switch_receiving_address_desc">Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</string>
|
||||
<string name="choose_file_title">Vybrat soubor</string>
|
||||
<string name="connect_via_link_incognito">Spojit se inkognito</string>
|
||||
<string name="turn_off_battery_optimization_button">Povolit</string>
|
||||
<string name="disable_notifications_button">Vypnout upozornění</string>
|
||||
<string name="turn_off_system_restriction_button">Otevřít nastavení aplikace</string>
|
||||
@@ -1320,7 +1265,6 @@
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Nový náhodný profil bude sdílen.</string>
|
||||
<string name="receipts_groups_enable_for_all">Povolit pro všechny skupiny</string>
|
||||
<string name="no_selected_chat">Žádný vybraný chat</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Vložte odkaz který jste obdrželi, pro spojení se svým kontaktem…</string>
|
||||
<string name="no_info_on_delivery">Žádné informace o doručení</string>
|
||||
<string name="receipts_groups_override_disabled">Odesílání doručenky je zakázáno pro %d skupin</string>
|
||||
<string name="receipts_groups_disable_for_all">Vypnout pro všechny skupiny</string>
|
||||
@@ -1421,7 +1365,6 @@
|
||||
<string name="refresh_qr_code">Obnovit</string>
|
||||
<string name="new_mobile_device">Nový telefon</string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Pouze jedno zařízení může pracovat současně</string>
|
||||
<string name="you_can_make_address_visible_via_settings">Můžete ji svým SimpleX kontaktům zviditelnit v Nastavení.</string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Již se připojujete ke skupině <b>%1$s</b>.]]></string>
|
||||
<string name="v5_4_link_mobile_desktop">Propojit mobilní a stolní aplikace! 🔗</string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">To je váš vlastní jednorázový odkaz!</string>
|
||||
@@ -1781,8 +1724,6 @@
|
||||
<string name="cannot_share_message_alert_title">Nelze odeslat zprávu</string>
|
||||
<string name="file_error">Chyba souboru</string>
|
||||
<string name="network_smp_proxy_mode_never_description">NEpoužívat soukromé směrování.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Záložní směrování zpráv</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Režim přeposílání zpráv</string>
|
||||
<string name="network_smp_proxy_mode_never">Nikdy</string>
|
||||
<string name="error_initializing_web_view">Chyba inicializace WebView. Aktualizujte systém na novou verzi. Prosím kontaktujte vývojáře.
|
||||
\nChyba: %s</string>
|
||||
@@ -1849,8 +1790,6 @@
|
||||
<string name="smp_proxy_error_connecting">Chyba připojení k přeposílajícímu serveru %1$s. Prosím, zkuste to později.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Předávacímu serveru %1$s se nepodařilo připojit k cílovému serveru %2$s. Prosím, zkuste to později.</string>
|
||||
<string name="cannot_share_message_alert_text">Vybrané nastavení chatu zakazuje tuto zprávu.</string>
|
||||
<string name="smp_servers_other">Jiné SMP servery</string>
|
||||
<string name="smp_servers_configured">Nastavené SMP servery</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Probíhá</string>
|
||||
<string name="chunks_uploaded">Části nahrány</string>
|
||||
<string name="n_file_errors">%1$d chyba souboru:\n%2$s</string>
|
||||
@@ -1920,7 +1859,6 @@
|
||||
<string name="acknowledged">Potvrzeno</string>
|
||||
<string name="duplicates_label">duplikáty</string>
|
||||
<string name="deleted">Smazán</string>
|
||||
<string name="open_server_settings_button">Otevřít nastavení serveru</string>
|
||||
<string name="v6_0_new_chat_experience">Nový zážitek z chatu 🎉</string>
|
||||
<string name="v6_0_new_media_options">Nové možnosti médií</string>
|
||||
<string name="new_message">Nová zpráva</string>
|
||||
@@ -1937,7 +1875,6 @@
|
||||
<string name="v6_1_message_dates_descr">Lepší datování zpráv.</string>
|
||||
<string name="v6_1_better_security">Lepší zabezpečení ✅</string>
|
||||
<string name="chunks_deleted">Části odstraněny</string>
|
||||
<string name="info_view_connect_button">připojení</string>
|
||||
<string name="current_user">Aktuální profil</string>
|
||||
<string name="servers_info_reconnect_server_error">Chyba znovu připojení serveru</string>
|
||||
<string name="servers_info_reconnect_servers_error">Chyba při opětovném připojování serverů</string>
|
||||
@@ -1949,7 +1886,6 @@
|
||||
<string name="switching_profile_error_title">Chyba přepínání profilu</string>
|
||||
<string name="select_chat_profile">Vyberte profil chatu</string>
|
||||
<string name="info_view_message_button">zpráva</string>
|
||||
<string name="info_view_open_button">otevřít</string>
|
||||
<string name="contact_deleted">Kontakt smazán!</string>
|
||||
<string name="member_info_member_inactive">neaktivní</string>
|
||||
<string name="servers_info_details">Detaily</string>
|
||||
@@ -2026,10 +1962,8 @@
|
||||
<string name="selected_chat_items_selected_n">Vybrány %d</string>
|
||||
<string name="privacy_media_blur_radius_medium">Střední</string>
|
||||
<string name="servers_info_subscriptions_section_header">Příjem zpráv</string>
|
||||
<string name="xftp_servers_configured">Nastavené XFTP servery</string>
|
||||
<string name="media_and_file_servers">Servery médií a souborů</string>
|
||||
<string name="message_servers">Servery zpráv</string>
|
||||
<string name="xftp_servers_other">Jiné FXTP servery</string>
|
||||
<string name="action_button_add_members">Pozvat</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Pošlete zprávu pro povolení volání.</string>
|
||||
<string name="accept_conditions">Přijmout podmínky</string>
|
||||
@@ -2058,7 +1992,6 @@
|
||||
<string name="no_message_servers_configured_for_receiving">Žádné servery pro příjem zpráv.</string>
|
||||
<string name="delete_chat_list_warning">Všechny chaty budou ze seznamu %s odebrány, a seznam bude smazán</string>
|
||||
<string name="for_social_media">Pro sociální sítě</string>
|
||||
<string name="remote_hosts_section">Vzdálené telefony</string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Podmínky budou přijaty pro operátora(y): <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Stejné podmínky budou platit pro operátora(y): <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Stejné podmínky budou platit pro operátora <b>%s</b>.]]></string>
|
||||
@@ -2144,7 +2077,6 @@
|
||||
<string name="appearance_app_toolbars">Panel nástrojů aplikace</string>
|
||||
<string name="appearance_bars_blur_radius">Rozmazání</string>
|
||||
<string name="direct_messages_are_prohibited">Přímé zprávy mezi členy jsou zakázány.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">pro lepší ochranu metadat.</string>
|
||||
<string name="connect_plan_chat_already_exists">Chat již existuje</string>
|
||||
<string name="proxied">Přes proxy</string>
|
||||
<string name="v6_2_business_chats">Firemní chaty</string>
|
||||
@@ -2191,13 +2123,11 @@
|
||||
<string name="operators_conditions_will_also_apply"><![CDATA[Tyto podmínky budou také platit pro: <b>%s</b>.]]></string>
|
||||
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Již jste spojeni s <b>%1$s</b>.]]></string>
|
||||
<string name="network_session_mode_session">Relace aplikace</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Podmínky budou přijaty pro povolené operátory po 30 dnech.</string>
|
||||
<string name="sync_connection_desc">Připojení vyžaduje opětovné vyjednání šifrování.</string>
|
||||
<string name="encryption_renegotiation_in_progress">Probíhá opětovné vyjednávání o šifrování.</string>
|
||||
<string name="sync_connection_confirm">Opravit</string>
|
||||
<string name="sync_connection_question">Opravit připojení?</string>
|
||||
<string name="smp_servers_new_server">Nový server</string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Všechny zprávy a soubory jsou odeslány s <b>koncovým šifrováním</b>, s post-quantovým zabezpečením v přímých zprávách.]]></string>
|
||||
<string name="onboarding_notifications_mode_off_desc_short">Žádné služba na pozadí</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc_short">Kontrolovat zprávy každých 10 minut</string>
|
||||
<string name="error_creating_report">Chyba vytváření hlášení</string>
|
||||
@@ -2206,7 +2136,6 @@
|
||||
<string name="network_proxy_random_credentials">Použijte náhodné přihlašovací údaje</string>
|
||||
<string name="report_compose_reason_header_spam">Nahlásit spam: uvidí pouze skupinový moderátoři.</string>
|
||||
<string name="operator_review_conditions">Přečíst podmínky</string>
|
||||
<string name="operator_website">Webové stránky</string>
|
||||
<string name="subscribed">Odebírán</string>
|
||||
<string name="display_name_requested_to_connect">požádáno o připojení</string>
|
||||
<string name="report_reason_spam">Spam</string>
|
||||
@@ -2222,24 +2151,20 @@
|
||||
<string name="connection_error_quota">Nedoručené zprávy</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Tato zpráva byla smazána, nebo dosud nebyla přijata.</string>
|
||||
<string name="report_reason_alert_title">Důvod nahlášení?</string>
|
||||
<string name="report_archive_alert_desc">Report bude archivován.</string>
|
||||
<string name="moderate_messages_will_be_marked_warning">Zprávy budou pro všechny členy označeny jako moderované.</string>
|
||||
<string name="chat_list_group_reports">Reporty</string>
|
||||
<string name="report_compose_reason_header_profile">Nahlašte profil člena: uvidí pouze skupinový moderátoři.</string>
|
||||
<string name="report_compose_reason_header_community">Nahlásit porušení: Uvidí pouze moderátoři skupiny.</string>
|
||||
<string name="private_routing_show_message_status">Zobrazit stav zpráv</string>
|
||||
<string name="app_check_for_updates_button_skip">Přeskočit tuto verzi</string>
|
||||
<string name="chat_list_always_visible">Zobrazit seznam v novém okně</string>
|
||||
<string name="v6_0_upgrade_app">Aktualizovat aplikaci automaticky</string>
|
||||
<string name="chat_database_exported_not_all_files">Některé soubory nebyly exportovány</string>
|
||||
<string name="unread_mentions">Nepřečtené zmínky</string>
|
||||
<string name="share_address_publicly">Sdílet adresu veřejně</string>
|
||||
<string name="share_simplex_address_on_social_media">Sdílent SimpleX adresu na sociálních médiích.</string>
|
||||
<string name="share_1_time_link_with_a_friend">Sdílejte 1 rázový odkaz s přítelem</string>
|
||||
<string name="network_socks_proxy">SOCKS Proxy</string>
|
||||
<string name="app_check_for_updates_update_available">Dostupná aktualizace: %s</string>
|
||||
<string name="app_check_for_updates_stable">Stabilní</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Můžete nastavit operátory v nastavení sítě a serverů.</string>
|
||||
<string name="settings_message_shape_tail">Ocas</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Zastavíte přijímání zpráv z tohoto chatu. Chat historie bude zachována.</string>
|
||||
<string name="xftp_servers_per_user">Servery pro nové soubory vašeho aktuálního profilu chatu</string>
|
||||
@@ -2274,7 +2199,6 @@
|
||||
<string name="uploaded_files">Nahrané soubory</string>
|
||||
<string name="subscription_results_ignored">Odběr ignorován</string>
|
||||
<string name="srv_error_version">Verze serveru není kompatibilní s nastavením sítě.</string>
|
||||
<string name="address_creation_instruction">Pro pozdější vytvoření adresy, klepněte v menu na Vytvořit SimpleX adresu.</string>
|
||||
<string name="your_servers">Vaše servery</string>
|
||||
<string name="view_updated_conditions">Zobrazit aktualizované podmínky</string>
|
||||
<string name="group_reports_active_one">1 report</string>
|
||||
@@ -2301,8 +2225,6 @@
|
||||
<string name="upload_errors">Chyby nahrávání</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Soukromé směrování se používá k doručování zpráv těmto serverům, protože k nim nejste připojeni.</string>
|
||||
<string name="size">Velikost</string>
|
||||
<string name="onboarding_network_operators_review_later">Přečíst později</string>
|
||||
<string name="onboarding_network_operators_update">Aktualizovat</string>
|
||||
<string name="operator_servers_title">%s servery</string>
|
||||
<string name="servers_info_subscriptions_total">Celkem</string>
|
||||
<string name="network_error_broker_version_desc">Verze serveru není kompatibilní s vaší aplikací: %1$s.</string>
|
||||
@@ -2319,7 +2241,6 @@
|
||||
<string name="contact_list_header_title">Vaše kontakty</string>
|
||||
<string name="servers_info_private_data_disclaimer">Od %s.\nJsou všechna data uchovávána ve vašem zařízení.</string>
|
||||
<string name="onboarding_choose_server_operators">Serverový operátoři</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Vybrat provozovatele sítě pro použití.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Můžete nakonfigurovat servery v nastavení.</string>
|
||||
<string name="icon_descr_sound_muted">Zvuk ztlumen</string>
|
||||
<string name="v6_0_reachable_chat_toolbar_descr">Používat aplikaci jednou rukou.</string>
|
||||
@@ -2496,7 +2417,6 @@
|
||||
<string name="compose_view_send_request">Odeslat žádost</string>
|
||||
<string name="compose_view_send_request_without_message">Odeslat žádost bez zprávy</string>
|
||||
<string name="v6_4_support_chat_descr">Posílání soukromé zpětné vazby do skupin.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Pošlete kontaktu po připojení.</string>
|
||||
<string name="v6_4_1_welcome_contacts_descr">Nastavení bio profilu a uvítací zprávy.</string>
|
||||
<string name="share_old_address_alert_button">Sdílení staré adresy</string>
|
||||
<string name="share_old_link_alert_button">Sdílení starého odkazu</string>
|
||||
@@ -2511,7 +2431,6 @@
|
||||
<string name="share_profile_via_link_alert_text">Adresa bude krátká a Váš profil bude sdílen prostřednictvím adresy.</string>
|
||||
<string name="share_group_profile_via_link_alert_text">Odkaz bude krátký a profil skupiny bude sdílen prostřednictvím odkazu.</string>
|
||||
<string name="the_sender_will_not_be_notified">Odesílatel NEBUDE informován.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Toto nastavení je pro váš aktuální profil</string>
|
||||
<string name="time_to_disappear_is_set_only_for_new_contacts">Čas mizení, je nastaven pouze pro nové kontakty.</string>
|
||||
<string name="cant_send_commands_alert_text">Pro odeslání příkazů musíte být připojen.</string>
|
||||
<string name="context_user_picker_cant_change_profile_alert_message">Pro použití jiného profilu po pokusu o připojení, smažte chat a znovu použijte odkaz.</string>
|
||||
@@ -2532,7 +2451,6 @@
|
||||
<string name="button_delete_member_messages_question">Smazat zprávy člena?</string>
|
||||
<string name="delete_member_messages_confirmation">Smazat zprávy</string>
|
||||
<string name="content_filter_files">Soubory</string>
|
||||
<string name="content_filter_menu_item">Filtr</string>
|
||||
<string name="content_filter_images">Obrázky</string>
|
||||
<string name="content_filter_links">Odkazy</string>
|
||||
<string name="member_messages_will_be_deleted_cannot_be_undone">Zprávy člena budou smazány - nelze zrušit!</string>
|
||||
@@ -2582,7 +2500,6 @@
|
||||
<string name="channel_no_active_relays_try_later">Kanál nemá aktivní relé.\nProsím, zkuste se připojit později.</string>
|
||||
<string name="channel_link">Odkaz kanálu</string>
|
||||
<string name="chat_link_channel">Odkaz kanálu</string>
|
||||
<string name="button_channel_members">Členové kanálu</string>
|
||||
<string name="channel_display_name_field">Název kanálu</string>
|
||||
<string name="channel_preferences">Vlastnosti kanálu</string>
|
||||
<string name="channel_role_label">kanál</string>
|
||||
@@ -2637,7 +2554,6 @@
|
||||
<string name="error_prefix">Chyba</string>
|
||||
<string name="error_adding_relay">Chyba přidávání relé</string>
|
||||
<string name="error_creating_channel">Chyba vytváření kanálu</string>
|
||||
<string name="error_opening_channel">Chyba otevírání kanálu</string>
|
||||
<string name="rcv_msg_error_parse">chyba: %s</string>
|
||||
<string name="error_saving_channel_profile">Chyba ukládání profilu kanálu</string>
|
||||
<string name="error_sharing_channel">Chyba sdílení kanálu</string>
|
||||
@@ -2657,7 +2573,6 @@
|
||||
<string name="compose_view_join_channel">Připojit ke kanálu</string>
|
||||
<string name="button_leave_channel">Opustit kanál</string>
|
||||
<string name="leave_channel_question">Opustit kanál?</string>
|
||||
<string name="add_button">Přidat</string>
|
||||
<string name="add_relay_button">Přidat relé</string>
|
||||
<string name="add_relays_title">Přidat relé</string>
|
||||
<string name="allow_direct_messages_channel">Povolit odběratelům odesílání přímých zpráv.</string>
|
||||
@@ -2709,7 +2624,6 @@
|
||||
<string name="onboarding_on_your_phone">Na vašem telefonu, ne na serverech.</string>
|
||||
<string name="connect_plan_open_channel">Otevřít kanál</string>
|
||||
<string name="open_external_link_title">Otevřít externí odkaz?</string>
|
||||
<string name="connect_plan_open_new_channel">Otevřít nový kanál</string>
|
||||
<string name="onboarding_or_show_qr_code">Nebo ukažte QR osobně nebo prostřednictvím videohovoru.</string>
|
||||
<string name="onboarding_or_use_qr_code">Nebo použijte tento QR kód - tisk nebo zobrazit online.</string>
|
||||
<string name="member_info_section_title_owner">MAJITEL</string>
|
||||
@@ -2776,7 +2690,6 @@
|
||||
<string name="channel_owner_count_plural">%1$d vlastníci</string>
|
||||
<string name="channel_owners_contributors_count">%1$d vlastníků & přispěvatelů</string>
|
||||
<string name="badge_supported_simplex">%1$s podpořilo SimpleX Chat. Odznak prošel %2$s.</string>
|
||||
<string name="settings_section_title_about">O aplikaci</string>
|
||||
<string name="relay_status_rejected">odmítnuto</string>
|
||||
<string name="member_info_relay_status_rejected_by_operator">odmítnuto operátorem relé</string>
|
||||
<string name="advanced_options">Pokročilé možnosti</string>
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
<string name="color_secondary_variant">Yderligere sekundær</string>
|
||||
<string name="chat_list_add_list">Tilføj liste</string>
|
||||
<string name="compose_view_add_message">Tilføj besked</string>
|
||||
<string name="smp_servers_preset_add">Tilføj forudindstillede servere</string>
|
||||
<string name="users_add">Tilføj profil</string>
|
||||
<string name="address_section_title">Adresse</string>
|
||||
<string name="abort_switch_receiving_address_desc">Adresseændringen bliver annulleret. Den gamle modtageradresse bliver brugt.</string>
|
||||
@@ -101,8 +100,6 @@
|
||||
<string name="connect_use_new_incognito_profile">Brug ny inkognito -profil</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Din profil sendes til den kontakt, du har modtaget dette link fra.</string>
|
||||
<string name="you_will_join_group">Du opretter forbindelse til alle gruppemedlemmer.</string>
|
||||
<string name="connect_via_link_verb">Forbinde</string>
|
||||
<string name="connect_via_link_incognito">Tilslut inkognito</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle chats og meddelelser slettes - dette kan ikke fortrydes!</string>
|
||||
<string name="delete_chat_list_warning">Alle chats fjernes fra listen %s, og listen slettes</string>
|
||||
<string name="chat_theme_apply_to_all_modes">Alle farvetilstande</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="connect__a_new_random_profile_will_be_shared">En ny tilfældig profil deles.</string>
|
||||
<string name="report_reason_other">En anden grund</string>
|
||||
<string name="answer_call">Svaropkald</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Alle kan være vært for servere.</string>
|
||||
<string name="settings_section_title_app">App</string>
|
||||
<string name="onboarding_notifications_mode_service_desc_short">App løber altid i baggrunden</string>
|
||||
<string name="app_version_code">App Build: %s</string>
|
||||
@@ -241,12 +237,6 @@
|
||||
<string name="report_reason_illegal">Upassende indhold</string>
|
||||
<string name="report_reason_community">Overtrædelse af retningslinjer for fællesskabet</string>
|
||||
<string name="report_reason_profile">Upassende profil</string>
|
||||
<string name="error_saving_smp_servers">Fejl under lagring af SMP-servere</string>
|
||||
<string name="error_saving_xftp_servers">Fejl ved lagring af XFTP-servere</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Sørg for, at SMP-serveradresserne er i korrekt format, linjeseparerede og ikke duplikerede.</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Sørg for, at XFTP-serveradresserne er i korrekt format, linjeseparerede og ikke duplikerede.</string>
|
||||
<string name="error_loading_smp_servers">Fejl ved indlæsning af SMP-servere</string>
|
||||
<string name="error_loading_xftp_servers">Fejl ved indlæsning af XFTP-servere</string>
|
||||
<string name="error_setting_network_config">Fejl ved opdatering af netværkskonfigurationen</string>
|
||||
<string name="failed_to_parse_chat_title">Chatten kunne ikke indlæses</string>
|
||||
<string name="failed_to_parse_chats_title">Kunne ikke indlæse chats</string>
|
||||
@@ -438,7 +428,6 @@
|
||||
<string name="report_archive_alert_title">Arkivér rapport?</string>
|
||||
<string name="report_archive_alert_title_nth">Arkivér %d rapporter?</string>
|
||||
<string name="report_archive_alert_title_all">Arkivér alle rapporter?</string>
|
||||
<string name="report_archive_alert_desc">Rapporten vil blive arkiveret for dig.</string>
|
||||
<string name="report_archive_for_me">For mig</string>
|
||||
<string name="report_archive_for_all_moderators">Til alle moderatorer</string>
|
||||
<string name="ci_status_other_error">Fejl: %1$s</string>
|
||||
@@ -521,8 +510,6 @@
|
||||
<string name="icon_descr_sent_msg_status_send_failed">afsendelse mislykkedes</string>
|
||||
<string name="icon_descr_received_msg_status_unread">ulæst</string>
|
||||
<string name="personal_welcome">Velkommen %1$s!</string>
|
||||
<string name="welcome">Velkomst!</string>
|
||||
<string name="this_text_is_available_in_settings">Denne tekst er tilgængelig i indstillinger</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Indstillinger</string>
|
||||
<string name="contact_connection_pending">forbinder…</string>
|
||||
@@ -532,7 +519,6 @@
|
||||
<string name="group_preview_join_as">Deltag som %s</string>
|
||||
<string name="group_preview_rejected">afvist</string>
|
||||
<string name="group_connection_pending">forbinder…</string>
|
||||
<string name="tap_to_start_new_chat">Tryk for at starte en ny chat</string>
|
||||
<string name="chat_with_developers">Chat med udviklerne</string>
|
||||
<string name="you_have_no_chats">Du har ingen chats</string>
|
||||
<string name="loading_chats">Indlæser chats…</string>
|
||||
@@ -547,7 +533,6 @@
|
||||
<string name="contact_should_accept">kontakten skal acceptere…</string>
|
||||
<string name="connect_with_contact_name_question">Forbinde med %1$s?</string>
|
||||
<string name="search_or_paste_simplex_link">Søg eller indsæt SimpleX-link</string>
|
||||
<string name="address_creation_instruction">Tryk på Opret SimpleX-adresse i menuen for at oprette den senere.</string>
|
||||
<string name="no_selected_chat">Ingen chat valgt</string>
|
||||
<string name="selected_chat_items_nothing_selected">Intet er valgt</string>
|
||||
<string name="selected_chat_items_selected_n">Valgte %d</string>
|
||||
@@ -679,8 +664,6 @@
|
||||
<string name="change_automatic_chat_deletion_message">Denne handling kan ikke fortrydes - beskeder sendt og modtaget i denne chat tidligere end valgt vil blive slettet.</string>
|
||||
<string name="disable_automatic_deletion">Deaktiver sletning af beskeder</string>
|
||||
<string name="chat_ttl_options_footer">Slet chatbeskeder fra din enhed.</string>
|
||||
<string name="info_view_connect_button">forbinde</string>
|
||||
<string name="info_view_open_button">åbn</string>
|
||||
<string name="info_view_message_button">besked</string>
|
||||
<string name="info_view_call_button">opkald</string>
|
||||
<string name="info_view_search_button">søg</string>
|
||||
@@ -703,7 +686,6 @@
|
||||
<string name="icon_descr_server_status_connected">Forbundet</string>
|
||||
<string name="icon_descr_server_status_disconnected">Afbrudt</string>
|
||||
<string name="icon_descr_server_status_error">Fejl</string>
|
||||
<string name="icon_descr_server_status_pending">Indtil</string>
|
||||
<string name="switch_receiving_address_question">Ændre modtageradresse?</string>
|
||||
<string name="switch_receiving_address_desc">Modtageradressen vil blive ændret til en anden server. Adresseændringen vil blive gennemført, når afsenderen er online.</string>
|
||||
<string name="sync_connection_force_question">Genforhandle kryptering?</string>
|
||||
@@ -754,7 +736,6 @@
|
||||
<string name="auto_accept_contact">Auto-accept</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Auto-accept-kontaktanmodninger</string>
|
||||
<string name="auto_accept_images">Auto-accept-billeder</string>
|
||||
<string name="available_in_v51">\nFås i v5.1</string>
|
||||
<string name="back">Tilbage</string>
|
||||
<string name="color_background">Baggrund</string>
|
||||
<string name="bad_desktop_address">Dårlig skrivebordsadresse</string>
|
||||
@@ -828,7 +809,6 @@
|
||||
<string name="cant_send_message_to_member_alert_title">Kan ikke sende besked til gruppemedlem</string>
|
||||
<string name="v6_4_1_new_interface_languages_descr">Katalansk, indonesisk, rumænsk og vietnamesisk - takket være vores brugere!</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1-gang link kan bruges <i>med kun en kontakt</i> - Del personligt eller via enhver messenger.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Alle meddelelser og filer sendes <b>ende-til-ende krypteret</b> med sikkerhed efter kvantet i direkte meddelelser.]]></string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[En separat TCP -forbindelse (og SOCKS -legitimationsoplysning) vil blive brugt <b>for hver chatprofil, du har i appen</b>.]]></string>
|
||||
<string name="network_session_mode_entity_description"><![CDATA[En separat TCP -forbindelse (og SOCKS -legitimationsoplysninger) vil blive brugt <b>til hvert kontakt- og gruppemedlem</b>.\n<b> Bemærk </b>: Hvis du har mange forbindelser, kan dit batteri og trafikforbrug være væsentligt højere, og nogle forbindelser kan mislykkes.]]></string>
|
||||
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Tilføj kontakt</b>: Sådan opretter du et nyt invitationslink eller opretter forbindelse via et link, du har modtaget.]]></string>
|
||||
@@ -874,7 +854,6 @@
|
||||
<string name="channel_owner_count_singular">%1$d ejer</string>
|
||||
<string name="channel_owner_count_plural">%1$d ejere</string>
|
||||
<string name="relay_status_active">aktiv</string>
|
||||
<string name="add_button">Tilføj</string>
|
||||
<string name="add_relay_button">Tilføj relay</string>
|
||||
<string name="add_relays_title">Tilføj relays</string>
|
||||
<string name="relay_bar_owner_no_delivery">Tilføj relays for at genoprette beskedleveringen.</string>
|
||||
@@ -894,7 +873,6 @@
|
||||
<string name="channel_subscriber_count_singular">%1$d abonnent</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d abonnenter</string>
|
||||
<string name="badge_supported_simplex">%1$s støttede SimpleX Chat. Mærket udløb den %2$s.</string>
|
||||
<string name="settings_section_title_about">Om</string>
|
||||
<string name="relay_status_accepted">accepteret</string>
|
||||
<string name="v7_0_channels_contributors">Tilføj bidragsydere.</string>
|
||||
<string name="add_description">Tilføj beskrivelse</string>
|
||||
@@ -925,11 +903,6 @@
|
||||
<string name="network_error_unknown_ca">Fingeraftrykket i serveradressen matcher ikke certifikatet: %1$s.</string>
|
||||
<string name="proxy_destination_error_unknown_ca">Fingeraftrykket i destinations-serveradressen matcher ikke certifikatet: %1$s.</string>
|
||||
<string name="error_marking_member_support_chat_read">Fejl ved markering som læst</string>
|
||||
<string name="unsupported_channel_name">Ikke-understøttet navn på kanal</string>
|
||||
<string name="unsupported_contact_name">Ikke-understøttet navn på kontakt</string>
|
||||
<string name="channel_name_requires_newer_app_version">Forbindelse gennem kanal-navnet kræver en nyere version af appen.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Forbindelse gennem kontakt-navnet kræver en nyere version af appen.</string>
|
||||
<string name="please_upgrade_the_app">Opgrader appen.</string>
|
||||
<string name="simplex_name_error">Fejl i SimpleX-navn</string>
|
||||
<string name="simplex_name_no_servers_desc">Ingen af dine servere er sat op til at slå SimpleX-navne op. Konfigurer serverne, eller anvend et forbindelseslink.</string>
|
||||
<string name="simplex_name_not_found">Navn ikke fundet</string>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<string name="connect_via_group_link">Der Gruppe beitreten?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string>
|
||||
<string name="you_will_join_group">Sie werden mit allen Gruppenmitgliedern verbunden.</string>
|
||||
<string name="connect_via_link_verb">Verbinden</string>
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
<string name="server_connected">Verbunden</string>
|
||||
<string name="server_error">Fehler</string>
|
||||
@@ -48,8 +47,6 @@
|
||||
<string name="simplex_link_mode_browser">Über den Browser</string>
|
||||
<string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden rot markiert.</string>
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string>
|
||||
<string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerkkonfiguration.</string>
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Verbindungszeitüberschreitung</string>
|
||||
@@ -168,14 +165,11 @@
|
||||
<string name="icon_descr_received_msg_status_unread">Ungelesen</string>
|
||||
<!-- ChatListView.kt -->
|
||||
<string name="personal_welcome">Willkommen %1$s!</string>
|
||||
<string name="welcome">Willkommen!</string>
|
||||
<string name="this_text_is_available_in_settings">Dieser Text ist in den Einstellungen verfügbar.</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="contact_connection_pending">Verbinde…</string>
|
||||
<string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string>
|
||||
<string name="group_preview_join_as">Beitreten als %s</string>
|
||||
<string name="group_connection_pending">Verbinde…</string>
|
||||
<string name="tap_to_start_new_chat">Tippen, um einen neuen Chat zu starten</string>
|
||||
<string name="chat_with_developers">Chatten Sie mit den Entwicklern</string>
|
||||
<string name="you_have_no_chats">Sie haben keine Chats</string>
|
||||
<!-- ShareListView.kt -->
|
||||
@@ -223,7 +217,6 @@
|
||||
<string name="icon_descr_server_status_connected">Verbunden</string>
|
||||
<string name="icon_descr_server_status_disconnected">Getrennt</string>
|
||||
<string name="icon_descr_server_status_error">Fehler</string>
|
||||
<string name="icon_descr_server_status_pending">Ausstehend</string>
|
||||
<string name="switch_receiving_address_question">Empfängeradresse wechseln\?</string>
|
||||
<string name="switch_receiving_address_desc">Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist.</string>
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
@@ -245,12 +238,8 @@
|
||||
<string name="copied">In die Zwischenablage kopiert</string>
|
||||
<!-- NewChatSheet -->
|
||||
<string name="add_contact_or_create_group">Neuen Chat starten</string>
|
||||
<string name="share_one_time_link">Einmal-Einladungslink teilen</string>
|
||||
<string name="connect_via_link_or_qr">Per Link / QR-Code verbinden</string>
|
||||
<string name="scan_QR_code">QR-Code scannen</string>
|
||||
<string name="create_group">Geheime Gruppe erstellen</string>
|
||||
<string name="to_share_with_your_contact">(zum Teilen mit Ihrem Kontakt)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(Scannen oder Einfügen aus der Zwischenablage)</string>
|
||||
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Berechtigung verweigert!</string>
|
||||
@@ -319,25 +308,17 @@
|
||||
<!-- Connection info - ContactConnectionInfoView.kt -->
|
||||
<string name="show_QR_code">QR-Code anzeigen</string>
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Ungültiger QR-Code</string>
|
||||
<string name="this_QR_code_is_not_a_link">Dieser QR-Code beschreibt keinen Link!</string>
|
||||
<string name="invalid_contact_link">Ungültiger Link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Dieser Link ist kein gültiger Verbindungslink!</string>
|
||||
<string name="connection_request_sent">Verbindungsanfrage gesendet!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage angenommen wird. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.]]></string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
|
||||
\nan Ihren Kontakt gesendet</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.]]></string>
|
||||
<string name="share_invitation_link">Einmal-Einladungslink teilen</string>
|
||||
<!-- PasteToConnect.kt -->
|
||||
<string name="connect_via_link">Über einen Link verbinden</string>
|
||||
<string name="connect_button">Verbinden</string>
|
||||
<string name="paste_button">Einfügen</string>
|
||||
<string name="this_string_is_not_a_connection_link">Diese Zeichenfolge entspricht keinem gültigen Verbindungslink!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche „<b>In mobiler App öffnen</b>“.]]></string>
|
||||
<!-- CreateLinkView.kt -->
|
||||
<string name="one_time_link">Einmal-Einladungslink</string>
|
||||
<!-- settings - SettingsView.kt -->
|
||||
@@ -352,9 +333,7 @@
|
||||
<string name="send_us_an_email">Senden Sie uns eine E-Mail</string>
|
||||
<string name="chat_lock">SimpleX-Sperre</string>
|
||||
<string name="chat_console">Chat-Konsole</string>
|
||||
<string name="smp_servers">SMP-Server</string>
|
||||
<string name="smp_servers_preset_address">Voreingestellte Serveradresse</string>
|
||||
<string name="smp_servers_preset_add">Voreingestellte Server hinzufügen</string>
|
||||
<string name="smp_servers_add">Server hinzufügen</string>
|
||||
<string name="smp_servers_test_server">Teste Server</string>
|
||||
<string name="smp_servers_test_servers">Teste alle Server</string>
|
||||
@@ -363,8 +342,6 @@
|
||||
<string name="smp_servers_test_some_failed">Einige Server haben den Test nicht bestanden:</string>
|
||||
<string name="smp_servers_scan_qr">Scannen Sie den QR-Code des Servers</string>
|
||||
<string name="smp_servers_enter_manually">Geben Sie den Server manuell ein</string>
|
||||
<string name="smp_servers_preset_server">Voreingestellter Server</string>
|
||||
<string name="smp_servers_your_server">Ihr Server</string>
|
||||
<string name="smp_servers_your_server_address">Ihre Serveradresse</string>
|
||||
<string name="smp_servers_use_server">Server nutzen</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Für neue Verbindungen nutzen</string>
|
||||
@@ -377,7 +354,6 @@
|
||||
<string name="contribute">Unterstützen Sie uns</string>
|
||||
<string name="rate_the_app">Bewerten Sie die App</string>
|
||||
<string name="use_simplex_chat_servers__question">Verwenden Sie SimpleX-Chat-Server\?</string>
|
||||
<string name="your_SMP_servers">Ihre SMP-Server</string>
|
||||
<string name="using_simplex_chat_servers">Verwendung von SimpleX-Chat-Servern.</string>
|
||||
<string name="how_to">Anleitung</string>
|
||||
<string name="how_to_use_your_servers">Wie Sie Ihre Server nutzen</string>
|
||||
@@ -423,15 +399,11 @@
|
||||
<string name="save_and_notify_group_members">Speichern und Gruppenmitglieder benachrichtigen</string>
|
||||
<string name="exit_without_saving">Beenden ohne Speichern</string>
|
||||
<!-- Welcome Prompts - WelcomeView.kt -->
|
||||
<string name="you_control_your_chat">Sie haben volle Kontrolle über Ihren Chat!</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Die Messaging- und Anwendungsplattform zum Schutz Ihrer Privatsphäre und Sicherheit.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Wir speichern auf den Servern keinen Ihrer Kontakte und keine Ihrer Nachrichten (sobald einmal zugestellt).</string>
|
||||
<string name="create_profile">Profil erstellen</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Das Profil wird nur mit Ihren Kontakten geteilt.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Der angezeigte Name darf keine Leerzeichen enthalten.</string>
|
||||
<string name="display_name">Geben Sie Ihren Namen ein:</string>
|
||||
<string name="create_profile_button">Erstellen</string>
|
||||
<string name="about_simplex">Über SimpleX</string>
|
||||
<!-- markdown demo - MarkdownHelpView.kt -->
|
||||
<string name="how_to_use_markdown">Wie Sie Markdowns anwenden</string>
|
||||
@@ -461,20 +433,10 @@
|
||||
<string name="callstate_connected">Verbunden</string>
|
||||
<string name="callstate_ended">Beendet</string>
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">Die Zukunft des Messagings</string>
|
||||
<string name="privacy_redefined">Datenschutz neu definiert</string>
|
||||
<string name="first_platform_without_user_ids">Keine Benutzerkennungen.</string>
|
||||
<string name="immune_to_spam_and_abuse">Immun gegen Spam</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Sie entscheiden, wer sich mit Ihnen verbinden kann.</string>
|
||||
<string name="decentralized">Dezentral</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Jeder kann seine eigenen Server aufsetzen.</string>
|
||||
<string name="create_your_profile">Ihr Profil erstellen</string>
|
||||
<string name="make_private_connection">Stellen Sie eine private Verbindung her</string>
|
||||
<string name="how_it_works">Wie es funktioniert</string>
|
||||
<!-- How SimpleX Works -->
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Erfahren Sie in unserem <font color="#0088ff">GitHub-Repository</font> mehr dazu.]]></string>
|
||||
<!-- MakeConnection -->
|
||||
<string name="paste_the_link_you_received">Fügen Sie den erhaltenen Link ein</string>
|
||||
<!-- Call -->
|
||||
@@ -499,7 +461,6 @@
|
||||
<string name="accept_call_on_lock_screen">Akzeptieren</string>
|
||||
<string name="show_call_on_lock_screen">Anzeigen</string>
|
||||
<string name="no_call_on_lock_screen">Deaktivieren</string>
|
||||
<string name="your_ice_servers">Ihre ICE-Server</string>
|
||||
<string name="webrtc_ice_servers">WebRTC-ICE-Server</string>
|
||||
<string name="relay_server_protects_ip">Relais-Server schützen Ihre IP-Adresse, können aber die Anrufdauer erfassen.</string>
|
||||
<string name="relay_server_if_necessary">Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.</string>
|
||||
@@ -556,7 +517,6 @@
|
||||
<string name="settings_section_title_chats">Chats</string>
|
||||
<string name="settings_developer_tools">Entwicklertools</string>
|
||||
<string name="settings_experimental_features">Experimentelle Funktionen</string>
|
||||
<string name="settings_section_title_socks">SOCKS-Proxy</string>
|
||||
<string name="settings_section_title_icon">App-Icon</string>
|
||||
<string name="settings_section_title_themes">Design</string>
|
||||
<string name="settings_section_title_messages">Nachrichten und Dateien</string>
|
||||
@@ -933,7 +893,6 @@
|
||||
<string name="is_not_verified">%s wurde noch nicht überprüft</string>
|
||||
<string name="to_verify_compare">Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</string>
|
||||
<string name="onboarding_notifications_mode_title">Private Benachrichtigungen</string>
|
||||
<string name="use_chat">Chat verwenden</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Ihr Kontakt und Sie können beide verschwindende Nachrichten versenden.</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="v4_2_group_links">Gruppen-Links</string>
|
||||
@@ -974,7 +933,6 @@
|
||||
<string name="users_delete_all_chats_deleted">Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
|
||||
<string name="network_option_ping_count">PING-Zähler</string>
|
||||
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
|
||||
<string name="smp_servers_per_user">Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil</string>
|
||||
<string name="files_and_media_section">Dateien und Medien</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport-Isolation</string>
|
||||
@@ -1098,10 +1056,7 @@
|
||||
<string name="icon_descr_waiting_for_video">Auf das Video warten</string>
|
||||
<string name="waiting_for_video">Auf das Video warten</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">Das Video wird heruntergeladen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string>
|
||||
<string name="your_XFTP_servers">Ihre XFTP-Server</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string>
|
||||
<string name="error_loading_smp_servers">Fehler beim Laden der SMP-Server</string>
|
||||
<string name="error_xftp_test_server_auth">Der Server erfordert zum Hochladen eine Autorisierung. Bitte überprüfen Sie das Passwort.</string>
|
||||
<string name="smp_server_test_download_file">Datei herunterladen</string>
|
||||
<string name="smp_server_test_compare_file">Datei vergleichen</string>
|
||||
@@ -1119,7 +1074,6 @@
|
||||
<string name="change_lock_mode">Sperrmodus ändern</string>
|
||||
<string name="passcode_not_changed">Zugangscode wurde nicht geändert!</string>
|
||||
<string name="decryption_error">Entschlüsselungsfehler</string>
|
||||
<string name="error_loading_xftp_servers">Fehler beim Laden der XFTP-Server</string>
|
||||
<string name="la_lock_mode_passcode">Eingabe des Zugangscodes</string>
|
||||
<string name="la_enter_app_passcode">Zugangscode eingeben</string>
|
||||
<string name="la_no_app_password">Kein App-Zugangscode</string>
|
||||
@@ -1138,7 +1092,6 @@
|
||||
<string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string>
|
||||
<string name="la_auth_failed">Authentifizierung fehlgeschlagen</string>
|
||||
<string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string>
|
||||
<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>
|
||||
@@ -1146,7 +1099,6 @@
|
||||
<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>
|
||||
<string name="xftp_servers">XFTP-Server</string>
|
||||
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der vorherigen).
|
||||
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
@@ -1159,8 +1111,6 @@
|
||||
<string name="allow_calls_only_if">Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string>
|
||||
<string name="allow_your_contacts_to_call">Erlaubt es Ihren Kontakten Sie anzurufen.</string>
|
||||
<string name="audio_video_calls">Audio-/Video-Anrufe</string>
|
||||
<string name="available_in_v51">"
|
||||
\nVerfügbar in v5.1"</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Sowohl Sie als auch Ihr Kontakt können Anrufe tätigen.</string>
|
||||
<string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string>
|
||||
<string name="prohibit_calls">Audio-/Video-Anrufe nicht erlauben.</string>
|
||||
@@ -1192,13 +1142,11 @@
|
||||
<string name="learn_more">Mehr erfahren</string>
|
||||
<string name="scan_qr_to_connect_to_contact">Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden.</string>
|
||||
<string name="simplex_address">SimpleX-Adresse</string>
|
||||
<string name="you_can_accept_or_reject_connection">Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
|
||||
<string name="customize_theme_title">Design anpassen</string>
|
||||
<string name="theme_colors_section_title">Interface-Farben</string>
|
||||
<string name="add_address_to_your_profile">Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet.</string>
|
||||
<string name="all_your_contacts_will_remain_connected_update_sent">Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string>
|
||||
<string name="create_address_and_let_people_connect">Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.</string>
|
||||
<string name="create_simplex_address">SimpleX-Adresse erstellen</string>
|
||||
<string name="share_with_contacts">Mit SimpleX-Kontakten teilen</string>
|
||||
<string name="your_contacts_will_remain_connected">Ihre Kontakte bleiben weiterhin verbunden.</string>
|
||||
@@ -1212,10 +1160,8 @@
|
||||
<string name="share_address_with_contacts_question">Die Adresse mit SimpleX-Kontakten teilen?</string>
|
||||
<string name="stop_sharing">Teilen beenden</string>
|
||||
<string name="stop_sharing_address">Das Teilen der Adresse beenden\?</string>
|
||||
<string name="dont_create_address">Keine Adresse erstellt</string>
|
||||
<string name="email_invite_body">Hallo!
|
||||
\nVerbinden Sie sich per SimpleX Chat mit mir: %s</string>
|
||||
<string name="you_can_create_it_later">Sie können dies später erstellen</string>
|
||||
<string name="enter_welcome_message">Geben Sie eine Begrüßungsmeldung ein …</string>
|
||||
<string name="group_welcome_preview">Vorschau</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit %s verbinden zu lassen.</string>
|
||||
@@ -1236,7 +1182,6 @@
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Lesen Sie mehr dazu in der <font color="#0088ff">Benutzeranleitung</font>.]]></string>
|
||||
<string name="import_theme_error_desc">Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren Sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten.</string>
|
||||
<string name="auth_open_chat_profiles">Chat-Profile wechseln</string>
|
||||
<string name="you_can_share_your_address">Sie können Ihre Adresse als Link oder QR-Code teilen – jede Person kann sich mit Ihnen verbinden.</string>
|
||||
<string name="all_app_data_will_be_cleared">Werden die App-Daten komplett gelöscht.</string>
|
||||
<string name="empty_chat_profile_is_created">Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt.</string>
|
||||
<string name="if_you_enter_self_destruct_code">Wenn Sie Ihren Selbstzerstörungs-Zugangscode während des Öffnens der App eingeben:</string>
|
||||
@@ -1412,11 +1357,9 @@
|
||||
<string name="send_receipts_disabled_alert_msg">Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %1$d Mitglieder hat.</string>
|
||||
<string name="connect_via_member_address_alert_desc">An dieses Gruppenmitglied wird eine Verbindungsanfrage gesendet.</string>
|
||||
<string name="connect_via_member_address_alert_title">Direkt verbinden\?</string>
|
||||
<string name="connect_via_link_incognito">Inkognito verbinden</string>
|
||||
<string name="connect_use_current_profile">Aktuelles Chat-Profil nutzen</string>
|
||||
<string name="connect_use_new_incognito_profile">Neues Inkognito-Profil nutzen</string>
|
||||
<string name="system_restricted_background_in_call_warn"><![CDATA[Wählen Sie bitte in den App-Einstellungen <b>App-Akkuverbrauch</b> / <b>Unbeschränkt</b> , um Anrufe im Hintergrund zu führen.]]></string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Fügen Sie den erhaltenen Link ein, um sich mit Ihrem Kontakt zu verbinden…</string>
|
||||
<string name="connect__a_new_random_profile_will_be_shared">Es wird ein neues Zufallsprofil geteilt.</string>
|
||||
<string name="connect__your_profile_will_be_shared">Ihr Profil %1$s wird geteilt.</string>
|
||||
<string name="turn_off_battery_optimization_button">Erlauben</string>
|
||||
@@ -1586,7 +1529,6 @@
|
||||
<string name="app_was_crashed">Ansicht abgestürzt</string>
|
||||
<string name="error_showing_content">Fehler beim Anzeigen des Inhalts</string>
|
||||
<string name="error_showing_message">Fehler beim Anzeigen der Nachricht</string>
|
||||
<string name="you_can_make_address_visible_via_settings">Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen.</string>
|
||||
<string name="recent_history_is_not_sent_to_new_members">Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet.</string>
|
||||
<string name="retry_verb">Wiederholen</string>
|
||||
<string name="camera_not_available">Kamera nicht verfügbar</string>
|
||||
@@ -1848,11 +1790,9 @@
|
||||
<string name="network_smp_proxy_mode_unprotected">Ungeschützt</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Bei unbekannten Servern privates Routing nutzen.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">KEIN privates Routing nutzen.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Modus für das Nachrichten-Routing</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Ja</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Nein</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Wenn die IP-Adresse versteckt ist</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Fallback für das Nachrichten-Routing</string>
|
||||
<string name="private_routing_show_message_status">Nachrichtenstatus anzeigen</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Herabstufung erlauben</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Immer privates Routing nutzen.</string>
|
||||
@@ -1893,7 +1833,6 @@
|
||||
<string name="settings_section_title_chat_colors">Chat-Farben</string>
|
||||
<string name="settings_section_title_chat_theme">Chat-Design</string>
|
||||
<string name="wallpaper_scale_fit">Passend</string>
|
||||
<string name="chat_list_always_visible">Chat-Liste in einem neuen Fenster anzeigen</string>
|
||||
<string name="wallpaper_preview_hello_alice">Guten Nachmittag!</string>
|
||||
<string name="wallpaper_preview_hello_bob">Guten Morgen!</string>
|
||||
<string name="dark_mode_colors">Farben für die dunkle Variante</string>
|
||||
@@ -1936,10 +1875,6 @@
|
||||
<string name="private_routing_error">Fehler beim privaten Routing</string>
|
||||
<string name="member_inactive_desc">Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird.</string>
|
||||
<string name="message_forwarded_desc">Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet.</string>
|
||||
<string name="smp_servers_configured">Konfigurierte SMP-Server</string>
|
||||
<string name="xftp_servers_configured">Konfigurierte XFTP-Server</string>
|
||||
<string name="smp_servers_other">Andere SMP-Server</string>
|
||||
<string name="xftp_servers_other">Andere XFTP-Server</string>
|
||||
<string name="completed">Abgeschlossen</string>
|
||||
<string name="member_info_member_inactive">Inaktiv</string>
|
||||
<string name="servers_info_sessions_connected">Verbunden</string>
|
||||
@@ -1972,7 +1907,6 @@
|
||||
<string name="download_errors">Fehler beim Herunterladen</string>
|
||||
<string name="duplicates_label">Duplikate</string>
|
||||
<string name="expired_label">Abgelaufen</string>
|
||||
<string name="open_server_settings_button">Server-Einstellungen öffnen</string>
|
||||
<string name="other_errors">Andere Fehler</string>
|
||||
<string name="proxied">Proxy-vermittelt</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Fehler beim Empfang</string>
|
||||
@@ -2058,9 +1992,7 @@
|
||||
<string name="privacy_media_blur_radius_strong">Stark</string>
|
||||
<string name="privacy_media_blur_radius_medium">Mittel</string>
|
||||
<string name="privacy_media_blur_radius_off">Aus</string>
|
||||
<string name="info_view_connect_button">Verbinden</string>
|
||||
<string name="info_view_message_button">Nachricht</string>
|
||||
<string name="info_view_open_button">Öffnen</string>
|
||||
<string name="conversation_deleted">Chat-Inhalte gelöscht!</string>
|
||||
<string name="only_delete_conversation">Nur die Chat-Inhalte löschen</string>
|
||||
<string name="info_view_search_button">Suchen</string>
|
||||
@@ -2209,14 +2141,9 @@
|
||||
<string name="onboarding_choose_server_operators">Server-Betreiber</string>
|
||||
<string name="onboarding_network_operators">Netzwerk-Betreiber</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Die App verwendet für jede Unterhaltung Server von unterschiedlichen Betreibern, um Ihre Privatsphäre zu schützen.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chat-Server empfängt, wird Ihre App diese über einen Flux-Server versenden.</string>
|
||||
<string name="onboarding_network_operators_review_later">Später einsehen</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Wählen sie die zu nutzenden Netzwerk-Betreiber aus.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Sie können die Server über die Einstellungen konfigurieren.</string>
|
||||
<string name="onboarding_network_operators_continue">Weiter</string>
|
||||
<string name="onboarding_network_operators_update">Aktualisieren</string>
|
||||
<string name="network_preset_servers_title">Voreingestellte Server</string>
|
||||
<string name="operator_review_conditions">Nutzungsbedingungen einsehen</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Die Nutzungsbedingungen der aktivierten Betreiber werden am %s automatisch akzeptiert.</string>
|
||||
@@ -2225,7 +2152,6 @@
|
||||
<string name="operator_servers_title">%s Server</string>
|
||||
<string name="operator_info_title">Netzwerk-Betreiber</string>
|
||||
<string name="operator_use_operator_toggle_description">Verwende Server</string>
|
||||
<string name="operator_website">Webseite</string>
|
||||
<string name="use_servers_of_operator_x">Verwende %s</string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Dieselben Nutzungsbedingungen gelten auch für den Betreiber <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: <b>%s</b>.]]></string>
|
||||
@@ -2267,12 +2193,10 @@
|
||||
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Um die Server von <b>%s</b> zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]></string>
|
||||
<string name="error_accepting_operator_conditions">Fehler beim Akzeptieren der Nutzungsbedingungen</string>
|
||||
<string name="error_updating_server_title">Fehler beim Aktualisieren des Servers</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">für einen besseren Metadatenschutz.</string>
|
||||
<string name="smp_servers_new_server">Neuer Server</string>
|
||||
<string name="message_deleted_or_not_received_error_title">Keine Nachricht</string>
|
||||
<string name="no_message_servers_configured_for_private_routing">Keine Server für privates Nachrichten-Routing.</string>
|
||||
<string name="share_address_publicly">Die Adresse öffentlich teilen</string>
|
||||
<string name="address_creation_instruction">Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen.</string>
|
||||
<string name="connection_error_quota_desc">Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen.</string>
|
||||
@@ -2280,10 +2204,8 @@
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_on">Die Nutzungsbedingungen wurden am %s akzeptiert.</string>
|
||||
<string name="operator_conditions_failed_to_load">Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:</string>
|
||||
<string name="remote_hosts_section">Ferngesteuerte Mobiltelefone</string>
|
||||
<string name="chat_archive">Oder importieren Sie eine Archiv-Datei</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Hinweis für Geräte von Xiaomi</b>: Bitte aktivieren Sie in den System-Einstellungen die Option "Autostart", damit Benachrichtigungen funktionieren.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Alle Nachrichten und Dateien werden <b>Ende-zu-Ende-verschlüsselt</b> versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]></string>
|
||||
<string name="button_add_team_members">Team-Mitglieder aufnehmen</string>
|
||||
<string name="button_add_friends">Freunde aufnehmen</string>
|
||||
<string name="display_name_accepted_invitation">Einladung angenommen</string>
|
||||
@@ -2372,7 +2294,6 @@
|
||||
<string name="report_reason_spam">Spam</string>
|
||||
<string name="archive_verb">Archiv</string>
|
||||
<string name="report_reason_alert_title">Grund der Meldung?</string>
|
||||
<string name="report_archive_alert_desc">Die Meldung wird für Sie archiviert.</string>
|
||||
<string name="report_compose_reason_header_profile">Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen.</string>
|
||||
<string name="report_compose_reason_header_other">Anderes melden: Nur Gruppenmoderatoren werden es sehen.</string>
|
||||
<string name="report_compose_reason_header_community">Verstoß melden: Nur Gruppenmoderatoren werden es sehen.</string>
|
||||
@@ -2409,7 +2330,6 @@
|
||||
<string name="network_smp_web_port_section_title">TCP-Port für Nachrichtenübermittlung</string>
|
||||
<string name="max_group_mentions_per_message_reached">Sie können bis zu %1$s Mitglieder pro Nachricht erwähnen!</string>
|
||||
<string name="mute_all_chat">Alle stummschalten</string>
|
||||
<string name="unread_mentions">Ungelesene Erwähnungen</string>
|
||||
<string name="member_reports_are_prohibited">Melden von Nachrichten ist in dieser Gruppe nicht erlaubt.</string>
|
||||
<string name="report_archive_alert_title_all">Alle Meldungen archivieren?</string>
|
||||
<string name="report_archive_alert_title_nth">Archiviere %d Meldungen?</string>
|
||||
@@ -2534,7 +2454,6 @@
|
||||
<string name="compose_view_send_contact_request_alert_question">Kontakt-Anfrage senden?</string>
|
||||
<string name="compose_view_send_request">Anfrage senden</string>
|
||||
<string name="compose_view_send_request_without_message">Anfrage ohne Nachricht senden</string>
|
||||
<string name="sent_to_your_contact_after_connection">Wird nach der Verbindung an Ihren Kontakt gesendet.</string>
|
||||
<string name="share_group_profile_via_link">Gruppen-Link aktualisieren?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Aktualisieren</string>
|
||||
<string name="share_profile_via_link">Adresse aktualisieren?</string>
|
||||
@@ -2590,7 +2509,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">Kontaktanfragen in Gruppen</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Mitglied ist gelöscht - Anfrage kann nicht angenommen werden</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">Angefragte Verbindung von Gruppe %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Diese Einstellung gilt für Ihr aktuelles Profil</string>
|
||||
<string name="allow_files_and_media_only_if">Erlauben Sie Dateien und Medien nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Erlauben Sie Ihren Kontakten Dateien und Medien zu senden.</string>
|
||||
<string name="chat_banner_bot">Bot</string>
|
||||
@@ -2620,7 +2538,6 @@
|
||||
<string name="remove_member_delete_messages_confirmation">Mitglied entfernen und Nachrichten löschen</string>
|
||||
<string name="content_filter_all_messages">Alle Nachrichten</string>
|
||||
<string name="content_filter_files">Dateien</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="content_filter_images">Bilder</string>
|
||||
<string name="content_filter_links">Links</string>
|
||||
<string name="placeholder_search_files">Dateien suchen</string>
|
||||
@@ -2650,7 +2567,6 @@
|
||||
<string name="chat_banner_channel">Kanal</string>
|
||||
<string name="info_row_channel">Kanal</string>
|
||||
<string name="channel_link">Kanallink</string>
|
||||
<string name="button_channel_members">Kanal-Mitglieder</string>
|
||||
<string name="channel_display_name_field">Kanalname</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_channel_for_self_cannot_undo_warning">Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
@@ -2680,7 +2596,6 @@
|
||||
<string name="enter_relay_name">Relais-Name eingeben…</string>
|
||||
<string name="error_adding_relay">Fehler beim Hinzufügen des Relais</string>
|
||||
<string name="error_creating_channel">Fehler beim Erstellen des Kanals</string>
|
||||
<string name="error_opening_channel">Fehler beim Öffnen des Kanals</string>
|
||||
<string name="relay_conn_status_failed">Fehlgeschlagen</string>
|
||||
<string name="relay_status_failed">Fehlgeschlagen</string>
|
||||
<string name="relay_test_step_get_link">Link erhalten</string>
|
||||
@@ -2697,7 +2612,6 @@
|
||||
<string name="no_chat_relays_enabled">Es sind keine Chat-Relais aktiviert.</string>
|
||||
<string name="not_all_relays_connected">Es sind nicht alle Relais verbunden</string>
|
||||
<string name="connect_plan_open_channel">Kanal öffnen</string>
|
||||
<string name="connect_plan_open_new_channel">Neuen Kanal öffnen</string>
|
||||
<string name="member_info_section_title_owner">Eigentümer</string>
|
||||
<string name="channel_members_section_owners">Eigentümer und Mitwirkende</string>
|
||||
<string name="preset_relay_address">Voreingestellte Relais-Adresse</string>
|
||||
@@ -2876,7 +2790,6 @@
|
||||
<string name="link_previews_alert_desc_socks">Die Linkvorschau wird über einen SOCKS-Proxy angefordert. DNS-Abfragen können dennoch lokal über Ihren DNS-Resolver erfolgen.</string>
|
||||
<string name="one_hand_ui_top_bar">Obere Leiste</string>
|
||||
<string name="cancel_channel_alert_msg">Ihr neuer Kanal %1$s ist mit %2$d von %3$d Relais verbunden.\nBei Abbruch, wird der Kanal gelöscht. Sie können ihn später neu erstellen.</string>
|
||||
<string name="add_button">Hinzufügen</string>
|
||||
<string name="add_relay_button">Relais hinzufügen</string>
|
||||
<string name="add_relays_title">Relais hinzufügen</string>
|
||||
<string name="button_cancel_and_delete_channel">Kanal abbrechen und löschen</string>
|
||||
@@ -2913,7 +2826,6 @@
|
||||
<string name="channel_owner_count_plural">%1$d Eigentümer</string>
|
||||
<string name="channel_owners_contributors_count">%1$d Eigentümer und Mitwirkende</string>
|
||||
<string name="badge_supported_simplex">%1$s hat SimpleX Chat unterstützt. Das Abzeichen ist am %2$s abgelaufen.</string>
|
||||
<string name="settings_section_title_about">Über</string>
|
||||
<string name="relay_status_acknowledged_roster">Bestätigter Relaisbestand</string>
|
||||
<string name="webpage_code_footer">Fügen Sie diesen Code in Ihre Webseite ein. Er zeigt die Vorschau Ihres Kanals / Ihrer Gruppe an.</string>
|
||||
<string name="advanced_options">Erweiterte Optionen</string>
|
||||
@@ -2924,8 +2836,6 @@
|
||||
<string name="badge_unknown_key_title">Abzeichen ist nicht verifizierbar</string>
|
||||
<string name="channel_webpage">Kanal-Webseite</string>
|
||||
<string name="chat_data">Chat-Daten</string>
|
||||
<string name="channel_name_requires_newer_app_version">Die Verbindung über den Kanalnamen erfordert eine neuere App‑Version.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Die Verbindung über den Kontaktnamen erfordert eine neuere App‑Version.</string>
|
||||
<string name="settings_section_title_contact">Kontakt</string>
|
||||
<string name="group_member_role_member_channel">Mitwirkender</string>
|
||||
<string name="copy_code">Code kopieren</string>
|
||||
@@ -2937,7 +2847,6 @@
|
||||
<string name="webpage_url_footer">Dies wird Abonnenten angezeigt und zum Laden der Vorschau genutzt.</string>
|
||||
<string name="more_privacy">Weitere Privatsphäre</string>
|
||||
<string name="embed_only_your_page">Nur Ihre oben genannte Seite kann die Vorschau anzeigen.</string>
|
||||
<string name="please_upgrade_the_app">Bitte die App aktualisieren.</string>
|
||||
<string name="badge_invested">%s hat sich am SimpleX Chat-Crowdfunding beteiligt.</string>
|
||||
<string name="badge_supports_simplex">%s unterstützt SimpleX Chat.</string>
|
||||
<string name="group_member_role_observer_channel">Abonnent</string>
|
||||
@@ -2946,8 +2855,6 @@
|
||||
<string name="member_role_will_be_changed_with_notification_channel">Die Rolle wird auf %s geändert. Alle Abonnenten werden benachrichtigt.</string>
|
||||
<string name="badge_unverified_desc">Dieses Abzeichen konnte nicht verifiziert werden und ist möglicherweise nicht echt.</string>
|
||||
<string name="group_link_requires_newer_version">Diese Gruppe erfordert eine neuere App‑Version. Bitte aktualisieren Sie die App, um beizutreten.</string>
|
||||
<string name="unsupported_channel_name">Kanalname wird nicht unterstützt</string>
|
||||
<string name="unsupported_contact_name">Kontaktname wird nicht unterstützt</string>
|
||||
<string name="badge_unverified_title">Abzeichen nicht verifiziert</string>
|
||||
<string name="relays_no_web_support">Die verwendeten Chat‑Relais unterstützen keine Webseiten.</string>
|
||||
<string name="webpage_code">Webseiten-Code</string>
|
||||
@@ -2962,7 +2869,6 @@
|
||||
<string name="simplex_name_resolver_error_desc">Namensauflösungs-Fehler: %1$s</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">Der Server %1$s unterstützt keine Namensauflösung. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink.</string>
|
||||
<string name="set_simplex_name">SimpleX-Name einrichten</string>
|
||||
<string name="simplex_name">SimpleX-Name</string>
|
||||
<string name="simplex_name_error">Fehler beim SimpleX-Namen</string>
|
||||
<string name="simplex_name_not_verified">SimpleX-Name ist nicht verifiziert</string>
|
||||
<string name="simplex_name_no_valid_link_desc">Der SimpleX-Name %1$s wurde registriert, aber er hat keinen gültigen Link.</string>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="allow_verb">Επέτρεψε</string>
|
||||
<string name="accept_contact_button">Αποδοχή</string>
|
||||
<string name="accept_contact_incognito_button">Αποδοχή ανώνυμης περιήγησης</string>
|
||||
<string name="smp_servers_preset_add">Προσθήκη προκαθορισμένου διακομιστή</string>
|
||||
<string name="smp_servers_add_to_another_device">Προσθήκη σε άλλη συσκευή</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Όλες οι επαφές σου θα παραμείνουν ενεργές.</string>
|
||||
<string name="accept_call_on_lock_screen">Αποδοχή</string>
|
||||
@@ -61,11 +60,9 @@
|
||||
<string name="full_backup">Αντίγραφο δεδομένων εφαρμογής</string>
|
||||
<string name="send_disappearing_message_5_minutes">5 λεπτά</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Θα συνδεθείς όταν η συσκευή της επαφής σου είναι συνδεδεμένη, παρακαλώ περίμενε ή έλεγξε αργότερα!</string>
|
||||
<string name="your_ice_servers">Ο ICE διακομιστής σου</string>
|
||||
<string name="app_version_title">Έκδοση εφαρμογής</string>
|
||||
<string name="you_sent_group_invitation">Έστειλες πρόσκληση ομάδας</string>
|
||||
<string name="send_disappearing_message_1_minute">1 λεπτό</string>
|
||||
<string name="smp_servers_your_server">Ο διακομιστής σου</string>
|
||||
<string name="address_section_title">Διεύθυνση</string>
|
||||
<string name="abort_switch_receiving_address_confirm">Ακύρωση</string>
|
||||
<string name="back">Πίσω</string>
|
||||
@@ -73,10 +70,8 @@
|
||||
<string name="you_will_join_group">Θα συνδεθείς με όλα τα μέλη της ομάδας.</string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s θέλει να συνδεθεί μαζί σου μέσω</string>
|
||||
<string name="allow_message_reactions">Επέτρεψε τις αντιδράσεις σε μηνύματα.</string>
|
||||
<string name="your_XFTP_servers">Ο διακομιστής XFTP σου</string>
|
||||
<string name="smp_servers_your_server_address">Η διεύθυνση του διακομιστή σου</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Το προφίλ, επαφές και παραδομένα μηνύματα σου είναι αποθηκευμένα στην συσκευή σου.</string>
|
||||
<string name="your_SMP_servers">Ο διακομιστής SMP σου</string>
|
||||
<string name="allow_to_send_files">Επιτρέπεται να σταλούν αρχεία και μέσα.</string>
|
||||
<string name="incognito_random_profile">Το τυχαίο προφίλ σου</string>
|
||||
<string name="group_info_section_title_num_members">%1$s ΜΕΛΗ</string>
|
||||
@@ -103,7 +98,6 @@
|
||||
<string name="switch_receiving_address">Αλλαγή διεύθυνσης λήψης</string>
|
||||
<string name="auth_unavailable">Πιστοποίηση μη διαθέσιμη</string>
|
||||
<string name="change_verb">Άλλαξε</string>
|
||||
<string name="available_in_v51">\nΔιαθέσιμο στην έκδοση 5.1</string>
|
||||
<string name="icon_descr_call_ended">Τέλος κλήσης</string>
|
||||
<string name="settings_section_title_calls">Κλήσεις</string>
|
||||
<string name="auto_accept_contact">Αυτόματη αποδοχή</string>
|
||||
@@ -146,7 +140,6 @@
|
||||
<string name="contacts_can_mark_messages_for_deletion">Οι επαφές μπορούν να επισημάνουν μηνύματα προς διαγραφή, τα οποία θα μπορείς να τα δεις.</string>
|
||||
<string name="connect_via_invitation_link">Σύνδεση μέσω μιας εφάπαξ σύνδεσης;</string>
|
||||
<string name="button_create_group_link">Δημιουργία σύνδεσμου</string>
|
||||
<string name="connect_via_link_or_qr">Σύνδεση μέσω σύνδεσμο/κωδικό γρήγορης ανταπόκρισης</string>
|
||||
<string name="connection_error_auth">Σφάλμα σύνδεσης (πιστοποίηση)</string>
|
||||
<string name="group_member_status_connecting">συνδέεται</string>
|
||||
<string name="group_connection_pending">συνδέεται…</string>
|
||||
@@ -169,7 +162,6 @@
|
||||
<string name="connect_with_contact_name_question">Σύνδεση με %1$s?</string>
|
||||
<string name="create_your_profile">Δημιουργία του προφίλ σου</string>
|
||||
<string name="display_name_connecting">συνδέεται…</string>
|
||||
<string name="create_profile_button">Δημιουργία</string>
|
||||
<string name="contact_preferences">Προτιμήσεις επαφής</string>
|
||||
<string name="connected_mobile">Συνδεδεμένο κινητό</string>
|
||||
<string name="info_row_connection">Σύνδεση</string>
|
||||
@@ -177,7 +169,6 @@
|
||||
<string name="callstate_connecting">συνδέεται…</string>
|
||||
<string name="desktop_connection_terminated">Σύνδεση τερματίστηκε</string>
|
||||
<string name="server_connecting">Συνδε</string>
|
||||
<string name="connect_via_link_incognito">Σύνδεση ανώνυμης περιήγησης</string>
|
||||
<string name="display_name_connection_established">σύνδεση επετεύχθη</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">η επαφή δεν έχει κρυπτογράφηση από άκρη-σε-άκρη</string>
|
||||
<string name="chat_preferences_contact_allows">Η επαφή επιτρέπει</string>
|
||||
@@ -196,13 +187,11 @@
|
||||
<string name="rcv_group_event_member_created_contact">αιτούμενη σύνδεση</string>
|
||||
<string name="your_privacy">Η ιδιωτικότητά σου</string>
|
||||
<string name="contact_sent_large_file">Η επαφή σου έστειλε ένα αρχείο το οποίο είναι μεγαλύτερο από το παρόν υποστηριζόμενο μέγεθος (%1$s).</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Το προφίλ της συνομιλίας σου θα σταλεί\nστην επαφή σου</string>
|
||||
<string name="connect_use_new_incognito_profile">Χρήση νέου ανώνυμου προφίλ</string>
|
||||
<string name="connect_plan_already_connecting">Ήδη συνδέεται</string>
|
||||
<string name="you_rejected_group_invitation">Απέρριψες την πρόσκληση της ομάδας</string>
|
||||
<string name="connect_via_contact_link">Σύνδεση μέσω διεύθυνση επαφής</string>
|
||||
<string name="connect_use_current_profile">Χρήση του τρέχοντος προφίλ</string>
|
||||
<string name="connect_via_link_verb">Συνδέσου</string>
|
||||
<string name="your_current_profile">Το τρέχον προφίλ σου</string>
|
||||
<string name="snd_group_event_member_deleted">αφαίρεσες %1$s</string>
|
||||
<string name="connect_via_group_link">Συμμετοχή ομάδας;</string>
|
||||
@@ -276,7 +265,6 @@
|
||||
<string name="network_smp_proxy_mode_always">Πάντα</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Η ενημέρωση της εφαρμογής κατέβηκε</string>
|
||||
<string name="app_check_for_updates_notice_title">Έλεγχος για ενημερώσεις</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Οποιοσδήποτε μπορεί να φιλοξενήσει διακομιστές.</string>
|
||||
<string name="audio_call_no_encryption">κλήση ήχου (χωρίς κρυπτογράφηση e2e)</string>
|
||||
<string name="call_on_lock_screen">Κλήσεις στην οθόνη κλειδώματος:</string>
|
||||
<string name="call_service_notification_audio_call">Κλήση ήχου</string>
|
||||
@@ -461,7 +449,6 @@
|
||||
<string name="cant_send_message_generic">δεν μπορείς να στείλεις μηνύματα</string>
|
||||
<string name="v6_4_1_new_interface_languages_descr">Καταλανικά, Ινδονησιακά, Ρουμανικά και Βιετναμέζικα – ευχαριστούμε τους χρήστες μας!</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Ο σύνδεσμος 1-χρήσης, μπορεί να χρησιμοποιηθεί <i>με μία μόνο επαφή</i> - προσωπικό διαμοιρασμό ή μέσω οποιασδήποτε εφαρμογής μηνυμάτων.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[Όλα τα μηνύματα και τα αρχεία αποστέλλονται <b>με κρυπτογράφηση από άκρη-σε-άκρη</b> και με μετα-κβαντική ασφάλεια σε άμεσα μηνύματα.]]></string>
|
||||
<string name="turn_off_battery_optimization"><![CDATA[<b>Επέτρεψε το</b> στο επόμενο παράθυρο διαλόγου για να λαμβάνεις ειδοποιήσεις άμεσα.]]></string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Συσκευές Xiaomi</b>: ενεργοποίησε το Αυτόματο Ξεκίνημα στις ρυθμίσεις συστήματος για να λειτουργούν οι ειδοποιήσεις.]]></string>
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[Οι όροι γίνονται αποδεκτοί για τον/τους χειριστή/ές: <b>%s</b>.]]></string>
|
||||
@@ -471,8 +458,6 @@
|
||||
<string name="remote_host_error_bad_state"><![CDATA[Η σύνδεση με το κινητό <b>%s</b> βρίσκεται σε κακή κατάσταση]]></string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 Υπολογιστής: σκάναρε τον εμφανιζόμενο QR κωδικό από την εφαρμογή, μέσω <b>Σάρωση QR κωδικού</b>.]]></string>
|
||||
<string name="remote_host_disconnected_from"><![CDATA[Αποσυνδέθηκε από το κινητό<b>%s</b> με τον λόγο: %s]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Αν δεν μπορείς να συναντηθείς προσωπικά, μπορείς να <b>σαρώσεις τον QR κωδικό στη βιντεοκλήση</b>, ή η επαφή σου να διαμοιραστεί ένα σύνδεσμο πρόσκλησης.]]></string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Αν δεν μπορείς να συναντηθείς προσωπικά,<b>δείξε τον QR κωδικό στη βιντεοκλήση</b>, ή μοιράσου το σύνδεσμο.]]></string>
|
||||
<string name="new_desktop"><![CDATA[<i>(νέο)</i>]]></string>
|
||||
<string name="this_device_version"><![CDATA[<i>(αυτή η συσκευή v%s)</i>]]></string>
|
||||
<string name="e2ee_info_e2ee"><![CDATA[Τα μηνύματα είναι προστατευμένα με <b>κρυπτογράφηση από άκρη-σε-άκρη</b>.]]></string>
|
||||
@@ -487,7 +472,6 @@
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 κινητό: πάτα <b>Άνοιγμα στην εφαρμογή κινητού</b>, μετά πάτα <b>Σύνδεση</b> μέσα στην εφαρμογή.]]></string>
|
||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Άνοιγμα <i>Χρήση από τον υπολογιστή</i> στην εφαρμογή του κινητού και σκάναρε τον QR κωδικό.]]></string>
|
||||
<string name="read_more_in_user_guide_with_link"><![CDATA[Διάβασε περισσότερα στον <font color="#0088ff">Οδηγό Χρήσης</font>.]]></string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Διάβασε περισσότερα στο <font color="#0088ff">αποθετήριό μας στο GitHub</font>.]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Ρύθμισε το <i>Χρήση .onion hosts</i> σε Όχι, αν ο διακομιστής μεσολάβησης SOCKS δεν τα υποστηρίζει.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Οι ίδιες προϋποθέσεις θα ισχύσουν και για τον χειριστή <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Οι ίδιες προϋποθέσεις θα ισχύσουν και για τον/τους χειριστή/στές <b>%s</b>.]]></string>
|
||||
@@ -504,7 +488,6 @@
|
||||
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Είσαι ήδη στην ομάδα <b>%1$s</b>.]]></string>
|
||||
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Ήδη προσπαθείς να μπεις στην ομάδα <b>%1$s</b>.]]></string>
|
||||
<string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Δεν <b>πρέπει να</b> χρησιμοποιείς την ίδια βάση δεδομένων σε δύο συσκευές.]]></string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Μπορείς επίσης να συνδεθείς κάνοντας κλικ στο σύνδεσμο. Αν ανοίξει στον browser, πάτα το <b>Άνοιγμα στην εφαρμογή κινητού</b> κουμπί.]]></string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Μπορείς να <font color="#0088ff">συνδεθείς με τους δημιουργούς του SimpleX Chat για να κάνεις ερωτήσεις και να λαμβάνεις ενημερώσεις</font>.]]></string>
|
||||
<string name="compose_view_send_contact_request_alert_text"><![CDATA[Θα μπορείς να στέλνεις μηνύματα <b>μόνο αφού γίνει αποδεκτό το αίτημά σου</b>.]]></string>
|
||||
<string name="change_automatic_deletion_question">Να αλλάξεις την αυτόματη διαγραφή μηνυμάτων;</string>
|
||||
@@ -543,11 +526,8 @@
|
||||
<string name="completed">Ολοκληρωμένο</string>
|
||||
<string name="operator_conditions_accepted_on">Οι όροι έγιναν αποδεκτοί στις: %s.</string>
|
||||
<string name="operator_conditions_of_use">Όροι χρήσης</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Οι όροι θα γίνουν αποδεκτοί για τους ενεργούς χειριστές μετά από 30 ημέρες.</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Οι όροι θα γίνουν αποδεκτοί στις: %s.</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Οι όροι θα γίνουν αυτόματα αποδεκτοί για τους ενεργούς χειριστές στις: %s.</string>
|
||||
<string name="smp_servers_configured">Διαμορφωμένοι SMP διακομιστές</string>
|
||||
<string name="xftp_servers_configured">Διαμορφωμένοι XFTP διακομιστές</string>
|
||||
<string name="configure_ICE_servers">Διαμορφωμένοι ICE διακομιστές</string>
|
||||
<string name="confirm_verb">Επιβεβαίωσε</string>
|
||||
<string name="confirm_delete_contact_question">Επιβεβαίωση διαγραφής επαφής;</string>
|
||||
@@ -560,7 +540,6 @@
|
||||
<string name="migrate_from_device_confirm_you_remember_passphrase">Επιβεβαίωσε ότι θυμάσαι τη φράση πρόσβασης της βάσης δεδομένων για να τη μεταφέρεις.</string>
|
||||
<string name="migrate_from_device_confirm_upload">Επιβεβαίωση μεταφόρτωσης</string>
|
||||
<string name="auth_confirm_credential">Επιβεβαίωση των διαπιστευτηρίων σου</string>
|
||||
<string name="info_view_connect_button">σύνδεση</string>
|
||||
<string name="smp_server_test_connect">Σύνδεση</string>
|
||||
<string name="connect_button">Σύνδεση</string>
|
||||
<string name="compose_view_connect">Σύνδεση</string>
|
||||
@@ -617,13 +596,11 @@
|
||||
<string name="settings_message_shape_corner">Γωνία</string>
|
||||
<string name="create_address_button">Δημιουργία</string>
|
||||
<string name="create_1_time_link">Δημιουργία συνδέσμου 1-χρήσης</string>
|
||||
<string name="create_address_and_let_people_connect">Δημιούργησε μια διεύθυνση για να μπορούν οι άλλοι να συνδεθούν μαζί σου.</string>
|
||||
<string name="created">Δημιουργήθηκε</string>
|
||||
<string name="info_row_created_at">Δημιουργήθηκε στις</string>
|
||||
<string name="share_text_created_at">Δημιουργήθηκε στις: %s</string>
|
||||
<string name="create_list">Δημιουργία λίστας</string>
|
||||
<string name="v5_3_new_desktop_app_descr">Δημιουργία νέου προφίλ στην εφαρμογή υπολογιστή. 💻</string>
|
||||
<string name="share_one_time_link">Δημιουργία συνδέσμου 1-χρήσης</string>
|
||||
<string name="smp_server_test_create_queue">Δημιουργία ουράς</string>
|
||||
<string name="v6_4_1_short_address_create">Δημιούργησε τη διεύθυνσή σου</string>
|
||||
<string name="migrate_from_device_creating_archive_link">Δημιουργία συνδέσμου αρχειοθέτησης</string>
|
||||
@@ -675,7 +652,6 @@
|
||||
<string name="ttl_day">%d ημέρα</string>
|
||||
<string name="ttl_days">%d ημέρες</string>
|
||||
<string name="info_row_debug_delivery">Αποστολή για αποσφαλμάτωση</string>
|
||||
<string name="decentralized">Αποκεντρωμένο</string>
|
||||
<string name="image_decoding_exception_title">Σφάλμα αποκωδικοποίησης</string>
|
||||
<string name="decryption_error">Σφάλμα αποκρυπτογράφησης</string>
|
||||
<string name="decryption_errors">σφάλματα αποκρυπτογράφησης</string>
|
||||
@@ -819,7 +795,6 @@
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">ΜΗΝ στέλνεις μηνύματα απευθείας, ακόμα κι αν ο δικός σου διακομιστής ή ο διακομιστής προορισμού δεν υποστηρίζει ιδιωτική δρομολόγηση.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Μην χρησιμοποιείς διαπιστευτήρια με το διακομιστή μεσολάβησης (proxy).</string>
|
||||
<string name="network_smp_proxy_mode_never_description">ΜΗΝ χρησιμοποιείς ιδιωτική δρομολόγηση.</string>
|
||||
<string name="dont_create_address">Μην δημιουργήσεις διεύθυνση</string>
|
||||
<string name="dont_enable_receipts">Μην ενεργοποιήσεις</string>
|
||||
<string name="v6_3_organize_chat_lists_descr">Μην χάσεις σημαντικά μηνύματα.</string>
|
||||
<string name="dont_show_again">Να μην εμφανιστεί ξανά</string>
|
||||
@@ -925,7 +900,6 @@
|
||||
<string name="icon_descr_flip_camera">Αντιστροφή κάμερας</string>
|
||||
<string name="appearance_font_size">Μέγεθος γραμματοσειράς</string>
|
||||
<string name="report_archive_for_all_moderators">Για όλους τους διαχειριστές</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">για καλύτερη ιδιωτικότητα μεταδεδομένων</string>
|
||||
<string name="for_chat_profile">Για το προφίλ συνομιλίας %s:</string>
|
||||
<string name="section_title_for_console">Για κονσόλα</string>
|
||||
<string name="for_everybody">Για όλους</string>
|
||||
@@ -1026,7 +1000,6 @@
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">Η εικόνα θα ληφθεί όταν η επαφή σου ολοκληρώσει τη μεταφόρτωσή της.</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Η εικόνα θα ληφθεί όταν η επαφή σου είναι συνδεδεμένη, περίμενε ή έλεγξε αργότερα!</string>
|
||||
<string name="la_immediately">Άμεσα</string>
|
||||
<string name="immune_to_spam_and_abuse">Ανοσοποιημένο στο spam</string>
|
||||
<string name="import_database_confirmation">Εισαγωγή</string>
|
||||
<string name="import_database_question">Εισαγωγή βάσης δεδομένων συνομιλίας;</string>
|
||||
<string name="import_database">Εισαγωγή βάσης δεδομένων</string>
|
||||
@@ -1081,7 +1054,6 @@
|
||||
<string name="invalid_message_format">μη έγκυρη διαμόρφωση μηνύματος</string>
|
||||
<string name="invalid_migration_confirmation">Μη έγκυρη επιβεβαίωση μετεγκατάστασης</string>
|
||||
<string name="invalid_name">Μη έγκυρο όνομα!</string>
|
||||
<string name="invalid_QR_code">Μη έγκυρος QR κωδικός</string>
|
||||
<string name="invalid_qr_code">Μη έγκυρος QR κωδικός</string>
|
||||
<string name="smp_servers_invalid_address">Μη έγκυρη διεύθυνση διακομιστή!</string>
|
||||
<string name="alert_title_group_invitation_expired">Η πρόσκληση έληξε!</string>
|
||||
@@ -1161,10 +1133,8 @@
|
||||
<string name="v5_2_disappear_one_message">Εξαφάνισε ένα μήνυμα</string>
|
||||
<string name="make_profile_private">Κάνε το προφίλ ιδιωτικό!</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Βεβαιώσου ότι έχεις σωστή διαμόρφωση του διακομιστή μεσολάβησης.</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Βεβαιώσου ότι οι διευθύνσεις του διακομιστή SMP έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες.</string>
|
||||
<string name="import_theme_error_desc">Βεβαιώσου ότι το αρχείο έχει σωστή σύνταξη YAML. Κάνε εξαγωγή ενός θέματος για να έχεις παράδειγμα της δομής αρχείου των θεμάτων.</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">"Βεβαιώσου ότι οι διευθύνσεις των διακομιστών WebRTC ICE έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες."</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Βεβαιώσου ότι οι διευθύνσεις των διακομιστών XFTP έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες.</string>
|
||||
<string name="v5_8_chat_themes_descr">Κάνε τις συνομιλίες σου να ξεχωρίζουν!</string>
|
||||
<string name="markdown_help">Βοήθεια στη Markdown σύνταξη</string>
|
||||
<string name="markdown_in_messages">Σύνταξη Markdown στα μηνύματα</string>
|
||||
@@ -1216,8 +1186,6 @@
|
||||
<string name="message_reactions_are_prohibited">Απαγορεύονται οι αντιδράσεις στα μηνύματα.</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">Απαγορεύονται οι αντιδράσεις στα μηνύματα σε αυτήν τη συνομιλία.</string>
|
||||
<string name="servers_info_subscriptions_section_header">Λήψη μηνυμάτων</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Εναλλακτική δρομολόγηση μηνυμάτων</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Λειτουργία δρομολόγησης μηνυμάτων</string>
|
||||
<string name="messages_section_title">Μηνύματα</string>
|
||||
<string name="settings_section_title_messages">Μηνύματα και αρχεία</string>
|
||||
<string name="message_servers">Διακομιστές μηνυμάτων</string>
|
||||
@@ -1289,7 +1257,6 @@
|
||||
<string name="chat_banner_connect_to_chat">Πάτα Σύνδεση για να συνομιλήσεις</string>
|
||||
<string name="chat_banner_send_request_to_connect">Πάτα Σύνδεση για αποστολή αιτήματος</string>
|
||||
<string name="chat_banner_connect_to_use_bot">Πάτα Σύνδεση για να χρησιμοποιήσεις το μποτ</string>
|
||||
<string name="address_creation_instruction">Πατήστε Δημιουργία διεύθυνσης SimpleX στο μενού, για να τη δημιουργήσετε αργότερα.</string>
|
||||
<string name="chat_banner_join_group">Πάτα Συμμετοχή στην ομάδα</string>
|
||||
<string name="tap_to_activate_profile">Πάτα για να ενεργοποιήσεις το προφίλ.</string>
|
||||
<string name="contact_tap_to_connect">Πάτα για Σύνδεση</string>
|
||||
@@ -1297,7 +1264,6 @@
|
||||
<string name="group_invitation_tap_to_join_incognito">Πάτα για ανώνυμη συμμετοχή</string>
|
||||
<string name="tap_to_paste_link">Πάτα για επικόλληση συνδέσμου</string>
|
||||
<string name="tap_to_scan">Πάτα για σάρωση</string>
|
||||
<string name="tap_to_start_new_chat">Πάτα για να ξεκινήσεις μία νέα συνομιλία</string>
|
||||
<string name="network_option_tcp_connection">Σύνδεση TCP</string>
|
||||
<string name="network_option_tcp_connection_timeout_background">Χρόνος λήξης σύνδεσης TCP στο παρασκήνιο</string>
|
||||
<string name="network_option_tcp_connection_timeout">Χρόνος λήξης σύνδεσης TCP</string>
|
||||
@@ -1326,7 +1292,6 @@
|
||||
<string name="unable_to_open_browser_desc">Για τις κλήσεις απαιτείται ο προεπιλεγμένος περιηγητής. Ρύθμισε τον προεπιλεγμένο περιηγητή στο σύστημα σου και μοιράσου περισσότερες πληροφορίες με τους προγραμματιστές.</string>
|
||||
<string name="this_device_name_shared_with_mobile">Το όνομα της συσκευής θα κοινοποιηθεί στην εφαρμογή του συνδεδεμένου κινητού.</string>
|
||||
<string name="sync_connection_force_desc">Η κρυπτογράφηση λειτουργεί και η νέα κρυπτογράφηση δεν είναι απαραίτητη. Μπορεί να προκαλέσει σφάλματα σύνδεσης!</string>
|
||||
<string name="next_generation_of_private_messaging">Το μέλλον στην ανταλλαγή μηνυμάτων</string>
|
||||
<string name="alert_text_msg_bad_hash">Ο κωδικός ελέγχου του προηγούμενου μηνύματος είναι διαφορετικός.</string>
|
||||
<string name="alert_text_msg_bad_id">Ο αναγνωριστικός κωδικός του επόμενου μηνύματος είναι λανθασμένος (μικρότερος ή ίσος με τον προηγούμενο).\nΑυτό μπορεί να συμβεί λόγω κάποιου σφάλματος ή όταν η σύνδεση έχει παραβιαστεί.</string>
|
||||
<string name="image_decoding_exception_desc">Η εικόνα δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε μια άλλη εικόνα ή επικοινώνησε με τους προγραμματιστές.</string>
|
||||
@@ -1337,11 +1302,9 @@
|
||||
<string name="moderate_messages_will_be_marked_warning">Τα μηνύματα θα επισημαίνονται ως ελεγχόμενα για όλα τα μέλη.</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Το μήνυμα θα διαγραφεί για όλα τα μέλη.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Το μήνυμα θα επισημανθεί ως υπό έλεγχο για όλα τα μέλη.</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Η πλατφόρμα μηνυμάτων και εφαρμογών που προστατεύει το απόρρητο και την ασφάλειά σου.</string>
|
||||
<string name="settings_is_storing_in_clear_text">Η φράση πρόσβασης αποθηκεύεται στις ρυθμίσεις ως απλό κείμενο.</string>
|
||||
<string name="passphrase_will_be_saved_in_settings">Η φράση πρόσβασης θα αποθηκευτεί στις ρυθμίσεις ως απλό κείμενο μετά την αλλαγή της ή την επανεκκίνηση της εφαρμογής.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Το προφίλ κοινοποιείται μόνο στις επαφές σου.</string>
|
||||
<string name="report_archive_alert_desc">Η αναφορά θα αρχειοθετηθεί για εσένα.</string>
|
||||
<string name="member_role_will_be_changed_with_notification_chat">Ο ρόλος θα αλλάξει σε %s. Όλοι οι συμμετέχοντες στη συνομιλία θα ειδοποιηθούν.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Ο ρόλος θα αλλάξει σε %s. Όλα τα μέλη της ομάδας θα ενημερωθούν.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Ο ρόλος θα αλλάξει σε %s. Το μέλος θα λάβει νέα πρόσκληση.</string>
|
||||
@@ -1369,15 +1332,11 @@
|
||||
<string name="alert_message_no_group">Αυτή η ομάδα δεν υπάρχει πλέον.</string>
|
||||
<string name="connect_plan_this_is_your_own_one_time_link">Αυτός είναι ο δικός σου σύνδεσμος 1-χρήσης!</string>
|
||||
<string name="connect_plan_this_is_your_own_simplex_address">Αυτή είναι η διεύθυνση σου SimpeX!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Αυτός ο σύνδεσμος δεν είναι έγκυρος!</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">Αυτός ο σύνδεσμος απαιτεί νεότερη έκδοση της εφαρμογής. Αναβάθμισε την εφαρμογή ή ζήτησε από την επαφή σου να σου στείλει ένα συμβατό σύνδεσμο.</string>
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">Αυτός ο σύνδεσμος χρησιμοποιήθηκε με άλλη κινητή συσκευή. Δημιούργησε ένα νέο σύνδεσμο στον υπολογιστή σου.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Αυτό το μήνυμα διαγράφηκε ή δεν έχει ληφθεί ακόμα.</string>
|
||||
<string name="this_QR_code_is_not_a_link">Αυτός ο κωδικός QR δεν είναι σύνδεσμος!</string>
|
||||
<string name="messages_section_description">Αυτή η ρύθμιση ισχύει για τα μηνύματα στο τρέχον προφίλ συνομιλίας σου.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Αυτή η ρύθμιση αφορά το τρέχον προφίλ σου.</string>
|
||||
<string name="this_string_is_not_a_connection_link">Αυτό το κείμενο δεν είναι σύνδεσμος!</string>
|
||||
<string name="this_text_is_available_in_settings">Αυτό το κείμενο είναι διαθέσιμο στις ρυθμίσεις</string>
|
||||
<string name="remote_ctrl_error_timeout">Εξαντλήθηκε ο χρόνος αναμονής κατά τη σύνδεση με τον υπολογιστή</string>
|
||||
<string name="time_to_disappear_is_set_only_for_new_contacts">Ο χρόνος εξαφάνισης ορίζεται μόνο για τις νέες επαφές.</string>
|
||||
<string name="color_title">Τίτλος</string>
|
||||
@@ -1394,13 +1353,11 @@
|
||||
<string name="v4_5_private_filenames_descr">Για την προστασία της ζώνης ώρας, τα αρχεία εικόνας/φωνής χρησιμοποιούν UTC ώρα.</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Για να προστατεύσεις τις πληροφορίες σου, ενεργοποίησε το SimpleX Lock.\nΘα σου ζητηθεί να ολοκληρώσεις την επαλήθευση ταυτότητας πριν ενεργοποιηθεί αυτή η λειτουργία.</string>
|
||||
<string name="private_routing_explanation">Για την προστασία της IP διεύθυνσής σου, η ιδιωτική δρομολόγηση χρησιμοποιεί τους διακομιστές SMP για την παράδοση μηνυμάτων.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Για την προστασία της ιδιωτικότητάς σου, το SimpleX χρησιμοποιεί ξεχωριστά αναγνωριστικά για κάθε μία από τις επαφές σου.</string>
|
||||
<string name="operator_use_for_messages_receiving">Για λήψη</string>
|
||||
<string name="enter_passphrase_notification_desc">Για να λαμβάνεις ειδοποιήσεις, παρακαλώ εισήγαγε τη φράση πρόσβασης της βάσης δεδομένων.</string>
|
||||
<string name="to_reveal_profile_enter_password">Για να αποκαλύψεις το κρυφό προφίλ σου, εισήγαγε έναν πλήρη κωδικό στο πεδίο αναζήτησης στη σελίδα Τα προφίλ συνομιλίας σου.</string>
|
||||
<string name="operator_use_for_sending">Για αποστολή</string>
|
||||
<string name="cant_send_commands_alert_text">Για αποστολή εντολών, θα πρέπει να είσαι συνδεδεμένος.</string>
|
||||
<string name="to_share_with_your_contact">(για διαμοιρασμό με την επαφή σου)</string>
|
||||
<string name="to_start_a_new_chat_help_header">Για να ξεκινήσεις μία νέα συνομιλία</string>
|
||||
<string name="servers_info_subscriptions_total">Συνολικά</string>
|
||||
<string name="context_user_picker_cant_change_profile_alert_message">Για να χρησιμοποιήσεις άλλο προφίλ μετά την προσπάθεια σύνδεσης, διέγραψε τη συνομιλία και χρησιμοποίησε ξανά τον σύνδεσμο.</string>
|
||||
@@ -1438,11 +1395,9 @@
|
||||
<string name="user_unmute">Απενεργοποίηση σίγασης</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Απροστάτευτο</string>
|
||||
<string name="icon_descr_received_msg_status_unread">αδιάβαστο</string>
|
||||
<string name="unread_mentions">Μη αναγνωσμένες αναφορές</string>
|
||||
<string name="unsupported_connection_link">Μη υποστηριζόμενος σύνδεσμος σύνδεσης</string>
|
||||
<string name="update_database">Αναβάθμιση</string>
|
||||
<string name="update_network_settings_confirmation">Αναβάθμιση</string>
|
||||
<string name="onboarding_network_operators_update">Αναβάθμιση</string>
|
||||
<string name="app_check_for_updates_update_available">Διαθέσιμη αναβάθμιση: %s</string>
|
||||
<string name="update_database_passphrase">Ενημέρωση φράσης πρόσβασης της βάσης δεδομένων</string>
|
||||
<string name="operator_updated_conditions">Ενημερωμένοι όροι</string>
|
||||
@@ -1450,7 +1405,6 @@
|
||||
<string name="app_check_for_updates_canceled">Η λήψη της ενημέρωσης ακυρώθηκε</string>
|
||||
<string name="profile_update_event_updated_profile">ενημερωμένο προφίλ</string>
|
||||
<string name="update_network_settings_question">Ενημέρωση ρυθμίσεων δικτύου;</string>
|
||||
<string name="update_network_session_mode_question">Ενημέρωση της λειτουργίας απομόνωσης μεταφοράς;</string>
|
||||
<string name="v6_4_1_short_address_update">Ενημέρωσε τη διεύθυνσή σου</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Η ενημέρωση των ρυθμίσεων θα επανασυνδέσει την εφαρμογή με όλους τους διακομιστές.</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Αναβάθμιση</string>
|
||||
@@ -1467,7 +1421,6 @@
|
||||
<string name="smp_server_test_upload_file">Ανέβασμα αρχείου</string>
|
||||
<string name="migrate_from_device_uploading_archive">Ανεβαίνει το αρχείο αρχειοθέτησης</string>
|
||||
<string name="recent_history_is_sent_to_new_members">Τα τελευταία 100 μηνύματα αποστέλλονται στα νέα μέλη.</string>
|
||||
<string name="use_chat">Χρήση συνομιλίας</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Χρησιμοποίησε διαφορετικά διαπιστευτήρια διακομιστή μεσολάβησης για κάθε σύνδεση.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Χρησιμοποίησε διαφορετικά διαπιστευτήρια διακομιστή μεσολάβησης για κάθε προφίλ.</string>
|
||||
<string name="network_disable_socks">Χρήση απευθείας σύνδεσης στο Διαδίκτυο;</string>
|
||||
@@ -1532,8 +1485,6 @@
|
||||
<string name="error_joining_group">Σφάλμα κατά τη συμμετοχή στην ομάδα</string>
|
||||
<string name="error_loading_chat_tags">Σφάλμα κατά τη φόρτωση των λιστών συνομιλιών</string>
|
||||
<string name="error_loading_details">Σφάλμα κατά τη φόρτωση των λεπτομερειών</string>
|
||||
<string name="error_loading_smp_servers">Σφάλμα κατά τη φόρτωση των διακομιστών SMP</string>
|
||||
<string name="error_loading_xftp_servers">Σφάλμα κατά τη φόρτωση των διακομιστών XFTP</string>
|
||||
<string name="error_marking_member_support_chat_read">Σφάλμα επισήμανσης ως αναγνωσμένου</string>
|
||||
<string name="unable_to_open_browser_title">Σφάλμα κατά το άνοιγμα του προγράμματος περιήγησης</string>
|
||||
<string name="error_preparing_contact">Σφάλμα κατά το άνοιγμα της συνομιλίας</string>
|
||||
@@ -1555,9 +1506,7 @@
|
||||
<string name="failed_to_save_servers">Σφάλμα κατά την αποθήκευση διακομιστών</string>
|
||||
<string name="migrate_from_device_error_saving_settings">Σφάλμα κατά την αποθήκευση των ρυθμίσεων</string>
|
||||
<string name="prefs_error_saving_settings">Σφάλμα κατά την αποθήκευση των ρυθμίσεων</string>
|
||||
<string name="error_saving_smp_servers">Σφάλμα κατά την αποθήκευση των διακομιστών SMP</string>
|
||||
<string name="error_saving_user_password">Σφάλμα κατά την αποθήκευση του κωδικού πρόσβασης χρήστη</string>
|
||||
<string name="error_saving_xftp_servers">Σφάλμα κατά την αποθήκευση διακομιστών XFTP</string>
|
||||
<string name="error_sending_message_contact_invitation">Σφάλμα κατά την αποστολή της πρόσκλησης</string>
|
||||
<string name="error_sending_message">Σφάλμα κατά την αποστολή του μηνύματος</string>
|
||||
<string name="error_setting_address">Σφάλμα κατά τη ρύθμιση της διεύθυνσης</string>
|
||||
@@ -1695,7 +1644,6 @@
|
||||
<string name="shutdown_alert_desc">Οι ειδοποιήσεις θα σταματήσουν να λειτουργούν μέχρι να επανεκκινήσεις την εφαρμογή.</string>
|
||||
<string name="cant_send_message_contact_not_synchronized">μη συγχρονισμένο</string>
|
||||
<string name="no_unread_chats">Δεν υπάρχουν μη αναγνωσμένες συνομιλίες</string>
|
||||
<string name="first_platform_without_user_ids">Χωρίς αναγνωριστικά χρήστη.</string>
|
||||
<string name="v4_6_group_moderation_descr">Τώρα οι διαχειριστές μπορούν:\n- να διαγράφουν τα μηνύματα των μελών.\n- να απενεργοποιούν μέλη (ρόλος παρατηρητή)</string>
|
||||
<string name="group_member_role_observer">παρατηρητής</string>
|
||||
<string name="chat_preferences_off">κλειστό</string>`
|
||||
@@ -1716,7 +1664,6 @@
|
||||
<string name="images_limit_desc">Μπορούν να σταλούν μόνο 10 εικόνες ταυτόχρονα</string>
|
||||
<string name="videos_limit_desc">Μπορούν να σταλούν μόνο 10 βίντεο ταυτόχρονα</string>
|
||||
<string name="only_chat_owners_can_change_prefs">Μόνο οι ιδιοκτήτες του chat μπορούν να αλλάξουν τις προτιμήσεις.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Μόνο οι συσκευές αποθηκεύουν προφίλ χρηστών, επαφές, ομάδες και μηνύματα.</string>
|
||||
<string name="only_delete_conversation">Διαγραφή μόνο της συνομιλίας</string>
|
||||
<string name="only_group_owners_can_change_prefs">Μόνο οι ιδιοκτήτες ομάδων μπορούν να αλλάξουν τις προτιμήσεις της ομάδας.</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Μόνο οι ιδιοκτήτες ομάδων μπορούν να ενεργοποιήσουν αρχεία και πολυμέσα.</string>
|
||||
@@ -1737,7 +1684,6 @@
|
||||
<string name="only_your_contact_can_send_disappearing">Μόνο η επαφή σου μπορεί να στείλει μηνύματα που εξαφανίζονται.</string>
|
||||
<string name="only_your_contact_can_send_files">Μόνο η επαφή σου μπορεί να στείλει αρχεία και πολυμέσα.</string>
|
||||
<string name="only_your_contact_can_send_voice">Μόνο η επαφή σου μπορεί να στείλει φωνητικά μηνύματα.</string>
|
||||
<string name="info_view_open_button">άνοιγμα</string>
|
||||
<string name="open_verb">Άνοιξε</string>
|
||||
<string name="rcv_group_event_open_chat">Άνοιγμα</string>
|
||||
<string name="turn_off_system_restriction_button">Άνοιξε τις ρυθμίσεις της εφαρμογής</string>
|
||||
@@ -1760,7 +1706,6 @@
|
||||
<string name="connect_plan_open_new_group">Άνοιξε νέα ομάδα</string>
|
||||
<string name="open_port_in_firewall_title">Άνοιγμα θύρας στο τείχος προστασίας</string>
|
||||
<string name="call_desktop_permission_denied_safari">Άνοιξε τις Ρυθμίσεις Safari / Ιστοσελίδες / Μικρόφωνο και στη συνέχεια επέλεξε Να επιτρέπεται για το localhost.</string>
|
||||
<string name="open_server_settings_button">Άνοιγμα ρυθμίσεων διακομιστή</string>
|
||||
<string name="permissions_open_settings">Άνοιγμα ρυθμίσεων</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Άνοιξε το SimpleX Chat για να αποδεχθείς την κλήση</string>
|
||||
<string name="open_to_accept">Άνοιξε για να αποδεχθείς</string>
|
||||
@@ -1782,8 +1727,6 @@
|
||||
<string name="other_label">άλλο</string>
|
||||
<string name="network_type_other">Άλλο</string>
|
||||
<string name="other_errors">άλλα σφάλματα</string>
|
||||
<string name="smp_servers_other">Άλλοι διακομιστές SMP</string>
|
||||
<string name="xftp_servers_other">Άλλοι διακομιστές XFTP</string>
|
||||
<string name="group_member_role_owner">ιδιοκτήτης</string>
|
||||
<string name="feature_roles_owners">ιδιοκτήτες</string>
|
||||
<string name="la_mode_passcode">Κωδικός πρόσβασης</string>
|
||||
@@ -1803,10 +1746,8 @@
|
||||
<string name="paste_link">Επικόλληση συνδέσμου</string>
|
||||
<string name="v5_5_simpler_connect_ui">Επικόλλησε το σύνδεσμο για να συνδεθείς!</string>
|
||||
<string name="paste_the_link_you_received">Επικόλλησε το σύνδεσμο που έλαβες</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Επικόλλησε το σύνδεσμο που έλαβες για να συνδεθείς με την επαφή σου…</string>
|
||||
<string name="call_connection_peer_to_peer">από άκρη-σε-άκρη</string>
|
||||
<string name="group_member_status_pending_approval_short">εκκρεμής</string>
|
||||
<string name="icon_descr_server_status_pending">Εκκρεμής</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Εκκρεμής</string>
|
||||
<string name="group_member_status_pending_approval">σε αναμονή έγκρισης</string>
|
||||
<string name="icon_descr_call_pending_sent">Εκκρεμής κλήση</string>
|
||||
@@ -1849,7 +1790,6 @@
|
||||
<string name="migrate_to_device_database_init">Προετοιμασία λήψης</string>
|
||||
<string name="migrate_from_device_database_init">Προετοιμασία μεταφόρτωσης</string>
|
||||
<string name="v4_5_message_draft_descr">Διατήρηση του τελευταίου πρόχειρου μηνύματος, με τα συνημμένα.</string>
|
||||
<string name="smp_servers_preset_server">Προκαθορισμένος διακομιστής</string>
|
||||
<string name="smp_servers_preset_address">Διεύθυνση προκαθορισμένου διακομιστή</string>
|
||||
<string name="network_preset_servers_title">Προκαθορισμένοι διακομιστές</string>
|
||||
<string name="network_smp_web_port_preset">Προκαθορισμένοι διακομιστές</string>
|
||||
@@ -1857,7 +1797,6 @@
|
||||
<string name="servers_info_previously_connected_servers_section_header">Προηγούμενοι συνδεδεμένοι διακομιστές</string>
|
||||
<string name="v6_2_business_chats_descr">Προστασία της ιδιωτικότητας των πελατών σου.</string>
|
||||
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Πολιτική απορρήτου και όροι χρήσης.</string>
|
||||
<string name="privacy_redefined">Επαναπροσδιορισμός της ιδιωτικότητας</string>
|
||||
<string name="privacy_and_security">Απόρρητο & ασφάλεια</string>
|
||||
<string name="onboarding_conditions_private_chats_not_accessible">Οι ιδιωτικές συνομιλίες, οι ομάδες και οι επαφές σου δεν είναι προσβάσιμες στους χειριστές του διακομιστή.</string>
|
||||
<string name="v4_5_private_filenames">Ιδιωτικά ονόματα αρχείων</string>
|
||||
@@ -1955,7 +1894,6 @@
|
||||
<string name="relay_server_if_necessary">Ο διακομιστής αναμετάδοσης χρησιμοποιείται μόνο αν είναι απαραίτητο. Οι άλλοι μπορούν να δουν τη διεύθυνση IP σου.</string>
|
||||
<string name="relay_server_protects_ip">Ο διακομιστής αναμετάδοσης προστατεύει τη διεύθυνση IP σου, αλλά μπορεί να παρακολουθεί τη διάρκεια της κλήσης.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Υπενθύμιση αργότερα</string>
|
||||
<string name="remote_hosts_section">Απομακρυσμένα κινητά τηλέφωνα</string>
|
||||
<string name="remove_passphrase">Κατάργηση</string>
|
||||
<string name="remove_member_confirmation">Κατάργηση</string>
|
||||
<string name="remove_member_delete_messages_confirmation">Κατάργηση και διαγραφή μηνυμάτων</string>
|
||||
@@ -2025,7 +1963,6 @@
|
||||
<string name="operator_review_conditions">Προϋποθέσεις ελέγχου</string>
|
||||
<string name="reviewed_by_admins">ελέγχθηκε από τους διαχειριστές</string>
|
||||
<string name="v6_4_review_members">Έλεγχος μελών ομάδας</string>
|
||||
<string name="onboarding_network_operators_review_later">Έλεγχος αργότερα</string>
|
||||
<string name="admission_stage_review">Έλεγχος μελών</string>
|
||||
<string name="admission_stage_review_descr">Έλεγχος μελών πριν την αποδοχή τους (knocking).</string>
|
||||
<string name="revoke_file__confirm">Ανάκληση</string>
|
||||
@@ -2070,7 +2007,6 @@
|
||||
<string name="wallpaper_scale">Κλιμάκωση στην οθόνη</string>
|
||||
<string name="scan_code">Σάρωση κωδικού</string>
|
||||
<string name="scan_from_mobile">Σάρωση από κινητό</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(σάρωσε ή επικόλλησε από το πρόχειρο)</string>
|
||||
<string name="scan_paste_link">Σάρωση / Επικόλληση συνδέσμου</string>
|
||||
<string name="scan_qr_code_from_desktop">Σάρωσε τον κωδικό QR από τον υπολογιστή</string>
|
||||
<string name="rcv_group_event_1_member_connected">%s συνδέθηκε</string>
|
||||
@@ -2088,7 +2024,6 @@
|
||||
<string name="select_contacts">Επέλεξε επαφές</string>
|
||||
<string name="cannot_share_message_alert_text">Οι επιλεγμένες προτιμήσεις συνομιλίας απαγορεύουν αυτό το μήνυμα.</string>
|
||||
<string name="selected_chat_items_selected_n">Επιλέχθηκαν %d</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Επέλεξε τους χειριστές δικτύου που θέλεις να χρησιμοποιήσεις.</string>
|
||||
<string name="self_destruct">Αυτοκαταστροφή</string>
|
||||
<string name="self_destruct_passcode">Κωδικός αυτοκαταστροφής</string>
|
||||
<string name="v5_1_self_destruct_passcode">Κωδικός αυτοκαταστροφής</string>
|
||||
@@ -2140,7 +2075,6 @@
|
||||
<string name="v4_4_disappearing_messages_desc">Τα αποσταλμένα μηνύματα θα διαγραφούν μετά από καθορισμένο χρονικό διάστημα.</string>
|
||||
<string name="color_sent_quote">Απεσταλμένη απάντηση</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Σύνολο απεσταλμένων</string>
|
||||
<string name="sent_to_your_contact_after_connection">Αποστέλλεται στην επαφή σου μετά τη σύνδεση.</string>
|
||||
<string name="sent_via_proxy">Αποστολή μέσω διακομιστή μεσολάβησης</string>
|
||||
<string name="network_session_mode_server">Διακομιστής</string>
|
||||
<string name="server_added_to_operator__name">Ο διακομιστής προστέθηκε στο χειριστή %s.</string>
|
||||
@@ -2204,7 +2138,6 @@
|
||||
<string name="v6_4_1_short_address">Σύντομη διεύθυνση SimpleX</string>
|
||||
<string name="show_call_on_lock_screen">Εμφάνιση</string>
|
||||
<string name="show_dev_options">Εμφάνιση:</string>
|
||||
<string name="chat_list_always_visible">Εμφάνιση λίστας μηνυμάτων σε νέο παράθυρο</string>
|
||||
<string name="terminal_always_visible">Εμφάνιση κονσόλας τερματικού σε νέο παράθυρο</string>
|
||||
<string name="notification_preview_mode_message_desc">Εμφάνιση επαφής και μηνύματος</string>
|
||||
<string name="show_developer_options">Εμφάνιση επιλογών για προγραμματιστές</string>
|
||||
@@ -2258,9 +2191,7 @@
|
||||
<string name="possible_slow_function_title">Αργή λειτουργία</string>
|
||||
<string name="receipts_section_groups">Μικρές ομάδες (μέγιστο 20 άτομα)</string>
|
||||
<string name="smp_server">Διακομιστής SMP</string>
|
||||
<string name="smp_servers">Διακομιστές SMP</string>
|
||||
<string name="network_socks_proxy">Διακομιστής μεσολάβησης SOCKS</string>
|
||||
<string name="settings_section_title_socks">Διακομιστής μεσολάβησης SOCKS</string>
|
||||
<string name="network_socks_proxy_settings">Ρυθμίσεις διακομιστή μεσολάβησης SOCKS</string>
|
||||
<string name="privacy_media_blur_radius_soft">Απαλό</string>
|
||||
<string name="chat_database_exported_not_all_files">Κάποιο/α αρχείο/α δεν εξήχθησαν</string>
|
||||
@@ -2377,10 +2308,7 @@
|
||||
<string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Προειδοποίηση: η έναρξη συνομιλίας σε πολλαπλές συσκευές δεν υποστηρίζεται και θα προκαλέσει σφάλματα στην παράδοση των μηνυμάτων.</string>
|
||||
<string name="database_downgrade_warning">Προειδοποίηση: ενδέχεται να χάσεις ορισμένα δεδομένα!</string>
|
||||
<string name="webrtc_ice_servers">Διακομιστές WebRTC ICE</string>
|
||||
<string name="operator_website">Ιστοσελίδα</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Δεν αποθηκεύουμε καμία από τις επαφές ή τα μηνύματά σου (αφού παραδοθούν) στους διακομιστές.</string>
|
||||
<string name="custom_time_unit_weeks">εβδομάδες</string>
|
||||
<string name="welcome">Καλωσόρισες!</string>
|
||||
<string name="personal_welcome">Καλωσόρισες %1$s!</string>
|
||||
<string name="button_welcome_message">Μήνυμα καλωσορίσματος</string>
|
||||
<string name="group_welcome_title">Μήνυμα καλωσορίσματος</string>
|
||||
@@ -2393,7 +2321,6 @@
|
||||
<string name="v5_7_call_sounds_descr">Κατά τη σύνδεση κλήσεων ήχου και βίντεο.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Όταν η IP είναι κρυφή</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Όταν είναι ενεργοποιημένοι περισσότεροι από ένας χειριστές, κανένας από αυτούς δεν διαθέτει μεταδεδομένα για να μάθει ποιος επικοινωνεί με ποιον.</string>
|
||||
<string name="you_can_accept_or_reject_connection">Όταν κάποιος ζητήσει να συνδεθεί, μπορείς να αποδεχτείς ή να απορρίψεις το αίτημα.</string>
|
||||
<string name="incognito_info_share">Όταν μοιράζεσε ένα ανώνυμο προφίλ με κάποιον, αυτό το προφίλ θα χρησιμοποιείται για τις ομάδες στις οποίες σε προσκαλούν.</string>
|
||||
<string name="network_type_network_wifi">WiFi</string>
|
||||
<string name="v5_7_quantum_resistant_encryption_descr">Θα ενεργοποιηθεί στις άμεσες συνομιλίες!</string>
|
||||
@@ -2409,7 +2336,6 @@
|
||||
<string name="file_error_auth">Λανθασμένο κλειδί ή άγνωστη διεύθυνση τμήματος αρχείου - πιθανότατα το αρχείο έχει διαγραφεί.</string>
|
||||
<string name="wrong_passphrase_title">Λανθασμένη φράση πρόσβασης!</string>
|
||||
<string name="xftp_server">Διακομιστής XFTP</string>
|
||||
<string name="xftp_servers">Διακομιστές XFTP</string>
|
||||
<string name="chat_preferences_yes">ναι</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Ναι</string>
|
||||
<string name="privacy_chat_list_open_links_yes">Ναι</string>
|
||||
@@ -2437,22 +2363,18 @@
|
||||
<string name="observer_cant_send_message_title">είσαι παρατηρητής</string>
|
||||
<string name="snd_group_event_member_blocked">μπλόκαρες %s</string>
|
||||
<string name="one_hand_ui_change_instruction">Μπορείς να το αλλάξεις στις ρυθμίσεις Εμφάνισης.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Μπορείς να διαμορφώσεις τους χειριστές στις ρυθμίσεις Δικτύου & διακομιστών.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Μπορείς να διαμορφώσεις τους διακομιστές μέσω των ρυθμίσεων.</string>
|
||||
<string name="maximum_message_size_reached_forwarding">Μπορείς να αντιγράψεις και να μειώσεις το μέγεθος του μηνύματος για να το στείλεις.</string>
|
||||
<string name="you_can_create_it_later">Μπορείς να το δημιουργήσεις αργότερα</string>
|
||||
<string name="you_can_enable_delivery_receipts_later">Μπορείς να το ενεργοποιήσεις αργότερα μέσω των Ρυθμίσεων.</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">Μπορείς να τις ενεργοποιήσεις αργότερα μέσω των ρυθμίσεων απορρήτου και ασφάλειας της εφαρμογής.</string>
|
||||
<string name="migrate_to_device_try_again">Μπορείς να δοκιμάσεις ξανά.</string>
|
||||
<string name="migrate_from_device_try_again">Μπορείς να δοκιμάσεις ξανά.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Μπορείς να αποκρύψεις ή να σιγάσεις ένα προφίλ χρήστη - κράτησέ το πατημένο για να εμφανιστεί το μενού.</string>
|
||||
<string name="you_can_make_address_visible_via_settings">Μπορείς να το κάνεις ορατό στις επαφές σου στο SimpleX μέσω των Ρυθμίσεων.</string>
|
||||
<string name="max_group_mentions_per_message_reached">Μπορείς να αναφέρεις εώς και %1$s μέλη ανά μήνυμα!</string>
|
||||
<string name="you_can_still_send_messages_to_contact">Μπορείς να στείλεις μηνύματα στην επαφή %1$s από τις αρχειοθετημένες επαφές.</string>
|
||||
<string name="you_can_set_connection_name_to_remember">Μπορείς να ορίσεις το όνομα της σύνδεσης για να θυμάσε με ποιον μοιράστηκες το σύνδεσμο.</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Μπορείς να μοιραστείς ένα σύνδεσμο ή έναν κωδικό QR - οποιοσδήποτε θα μπορεί να συμμετάσχει στην ομάδα. Δεν θα χάσεις μέλη της ομάδας αν τον διαγράψεις αργότερα.</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">Μπορείς να μοιραστείς αυτήν τη διεύθυνση με τις επαφές σου για να τους επιτρέψεις να συνδεθούν με την επαφή %s.</string>
|
||||
<string name="you_can_share_your_address">Μπορείς να διαμοιραστείς τη διεύθυνσή σου ως σύνδεσμο ή κωδικό QR - οποιοσδήποτε θα μπορεί να συνδεθεί μαζί σου.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Μπορείς να ξεκινήσεις τη συνομιλία μέσω της εφαρμογής Ρυθμίσεις / Βάση δεδομένων ή επανεκκινώντας την εφαρμογή.</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Μπορείς ακόμα να δεις τη συνομιλία με την επαφή %1$s, στη λίστα των συνομιλιών.</string>
|
||||
<string name="cant_send_message_alert_title">Δεν μπορείς να στείλεις μηνύματα!</string>
|
||||
@@ -2464,9 +2386,7 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">άλλαξες διεύθυνση για %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">άλλαξες ρόλο για τον εαυτό σου σε %s</string>
|
||||
<string name="snd_group_event_changed_member_role">άλλαξες το ρόλο του μέλους %s σε %s</string>
|
||||
<string name="you_control_your_chat">Έχεις τον έλεγχο της συνομιλίας σου!</string>
|
||||
<string name="la_could_not_be_verified">Δεν ήταν δυνατή η επαλήθευση. Παρακαλώ, δοκίμασε ξανά.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Εσύ αποφασίζεις ποιος μπορεί να συνδεθεί.</string>
|
||||
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Έχεις ήδη ζητήσει σύνδεση μέσω αυτής της διεύθυνσης!</string>
|
||||
<string name="you_have_no_chats">Δεν έχεις συνομιλίες</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Πρέπει να εισάγεις τη φράση πρόσβασης κάθε φορά που ξεκινά η εφαρμογή - δεν αποθηκεύεται στη συσκευή.</string>
|
||||
@@ -2513,7 +2433,6 @@
|
||||
<string name="appearance_zoom">Μεγέθυνση</string>
|
||||
<string name="content_filter_all_messages">Όλα τα μηνύματα</string>
|
||||
<string name="content_filter_files">Αρχεία</string>
|
||||
<string name="content_filter_menu_item">ΦΙλτράρισμα</string>
|
||||
<string name="content_filter_images">Εικόνες</string>
|
||||
<string name="content_filter_links">Σύνδεσμοι</string>
|
||||
<string name="placeholder_search_files">Αναζήτηση αρχείων</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user