mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 10:15:47 +00:00
supporter badges (#7350)
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/**"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,10 @@ 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 apiAckBadgeAlert(userId: Int64, badgePurchaseId: Int64, alertKind: BadgeAlertKind, snooze: Bool, episode: String)
|
||||
// misc
|
||||
case showVersion
|
||||
case getAgentSubsTotal(userId: Int64)
|
||||
@@ -413,6 +417,10 @@ 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 .apiAckBadgeAlert(userId, badgePurchaseId, alertKind, snooze, episode):
|
||||
return "/_badge ack \(userId) \(badgePurchaseId) \(alertKind.text) \(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 +608,9 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiUploadStandaloneFile: return "apiUploadStandaloneFile"
|
||||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||
case .apiRedeemBadgeCode: return "apiRedeemBadgeCode"
|
||||
case .apiGetBadgeState: return "apiGetBadgeState"
|
||||
case .apiAckBadgeAlert: return "apiAckBadgeAlert"
|
||||
case .showVersion: return "showVersion"
|
||||
case .getAgentSubsTotal: return "getAgentSubsTotal"
|
||||
case .getAgentServersSummary: return "getAgentServersSummary"
|
||||
@@ -653,6 +664,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
|
||||
}
|
||||
}
|
||||
@@ -1019,6 +1033,10 @@ 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)
|
||||
case badgeState(user: UserRef, badgeState: BadgeState?)
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
@@ -1070,6 +1088,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
case .archiveExported: "archiveExported"
|
||||
case .archiveImported: "archiveImported"
|
||||
case .appSettings: "appSettings"
|
||||
case .badgeRedeemed: "badgeRedeemed"
|
||||
case .badgeState: "badgeState"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,6 +1143,8 @@ 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): return withUser(u, "redeemedBadge: \(String(describing: redeemedBadge))\nnewBadge: \(newBadge)")
|
||||
case let .badgeState(u, badgeState): return withUser(u, String(describing: badgeState))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1205,6 +1227,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 +1302,8 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
||||
case .remoteCtrlConnected: "remoteCtrlConnected"
|
||||
case .remoteCtrlStopped: "remoteCtrlStopped"
|
||||
case .contactPQEnabled: "contactPQEnabled"
|
||||
case .badgeChanged: "badgeChanged"
|
||||
case .badgeAlert: "badgeAlert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,6 +1386,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?
|
||||
|
||||
@@ -2182,6 +2182,119 @@ func resetAgentServersStats() async throws {
|
||||
try await sendCommandOkResp(.resetAgentServersStats)
|
||||
}
|
||||
|
||||
// The failures redeemBadgeCode raises, as core flattens them to command error text.
|
||||
// tests/Bots/BadgeServiceTests.hs asserts that text, so a change in core breaks a test there.
|
||||
enum BadgeRedeemError: Error {
|
||||
case invalidCode
|
||||
case serviceNotConfigured
|
||||
case alreadyActive
|
||||
case codeInvalid
|
||||
case codeUsed
|
||||
case codeExpired
|
||||
case rateLimited
|
||||
case serviceFailed
|
||||
case badServiceResponse
|
||||
case credentialNotVerified
|
||||
case unsupportedVersion
|
||||
case networkError
|
||||
case badgeEnded
|
||||
case unknown
|
||||
}
|
||||
|
||||
private enum BadgeErrorText {
|
||||
static let invalidCode = "invalid badge code"
|
||||
static let serviceNotConfigured = "badge service not configured"
|
||||
static let alreadyActive = "badge already active"
|
||||
static let unknownKeyIndex = "unknown badge key index"
|
||||
static let credentialNotVerified = "badge credential does not verify against configured key"
|
||||
static let invalidResponse = "invalid badge service response, "
|
||||
static let unexpectedResponse = "unexpected badge service response: "
|
||||
static let serviceError = "badge service error: "
|
||||
// raised by sendServiceRequestTo, not by redeemBadgeCode itself, when the reply is not JSON
|
||||
static let undecodableResponse = "invalid service response"
|
||||
}
|
||||
|
||||
func badgeRedeemError(_ error: ChatError) -> BadgeRedeemError {
|
||||
// the app's own classifier decides what counts as a network failure; only the classification is
|
||||
// used here, not its retry policy - a client that retries makes code guessing cheaper
|
||||
if retryableNetworkErrorAlert(error) != nil { return .networkError }
|
||||
guard case let .error(.commandError(message)) = error else { return .unknown }
|
||||
switch message {
|
||||
case BadgeErrorText.invalidCode: return .invalidCode
|
||||
case BadgeErrorText.serviceNotConfigured: return .serviceNotConfigured
|
||||
case BadgeErrorText.alreadyActive: return .alreadyActive
|
||||
case BadgeErrorText.unknownKeyIndex, BadgeErrorText.credentialNotVerified: return .credentialNotVerified
|
||||
case BadgeErrorText.undecodableResponse: return .badServiceResponse
|
||||
default: break
|
||||
}
|
||||
if message.hasPrefix(BadgeErrorText.invalidResponse) || message.hasPrefix(BadgeErrorText.unexpectedResponse) {
|
||||
return .badServiceResponse
|
||||
}
|
||||
if message.hasPrefix(BadgeErrorText.serviceError) {
|
||||
return badgeServiceError(String(message.dropFirst(BadgeErrorText.serviceError.count)))
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// the service's own tag, which core bounds to [a-z0-9_] and 32 characters and which is never shown
|
||||
private func badgeServiceError(_ tag: String) -> BadgeRedeemError {
|
||||
switch tag {
|
||||
case "code_invalid": return .codeInvalid
|
||||
case "code_used": return .codeUsed
|
||||
case "code_expired": return .codeExpired
|
||||
case "rate_limited": return .rateLimited
|
||||
// retrying never succeeds: the client is too old for the service
|
||||
case "unsupported_version": return .unsupportedVersion
|
||||
default: return .serviceFailed
|
||||
}
|
||||
}
|
||||
|
||||
// log: false because the code is a bearer secret until it is redeemed - it is in the command, and a
|
||||
// service response echoed into an error message would carry it into the terminal with the response.
|
||||
func apiRedeemBadgeCode(_ userId: Int64, _ code: String) async throws -> (user: User, newBadge: Bool) {
|
||||
let r: APIResult<ChatResponse2> = await chatApiSendCmd(.apiRedeemBadgeCode(userId: userId, code: code), log: false)
|
||||
switch r {
|
||||
// redeemedBadge is dropped: it is the credential, and the user's profile carries what is shown
|
||||
case let .result(.badgeRedeemed(user, _, newBadge)): return (user, newBadge)
|
||||
case let .error(e): throw badgeRedeemError(e)
|
||||
default:
|
||||
// the response type alone - it names a case or a JSON key, never the service's message
|
||||
logger.error("apiRedeemBadgeCode: unexpected \(r.responseType)")
|
||||
throw BadgeRedeemError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
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 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 +2465,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 +2505,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 +3084,20 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .badgeChanged(user, badgeState):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
// read by core after retiring or presenting, so it carries the profile badge as changed
|
||||
m.updateUser(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,45 @@
|
||||
//
|
||||
// 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 private badges work")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("A badge is not an account. It is a signed credential stored on your device. It does not identify you, and no one keeps a record of who holds which badge.")
|
||||
Text("Your contacts see the badge and its expiry date, and nothing else. The badge carries no identifier, so it cannot be used to find out who you are or to match you across chats.")
|
||||
Text("Payment and badge are kept apart. Paying is one step; the badge is issued in another, under a key that exists only for that badge. Whoever handles the payment cannot see where the badge ends up.")
|
||||
}
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgesHowItWorksView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BadgesHowItWorksView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,228 @@
|
||||
//
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
.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 from your receipt.")
|
||||
.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)
|
||||
}
|
||||
|
||||
private func applyCodeInput(_ s: String) {
|
||||
let formatted = formatBadgeCodeInput(s)
|
||||
if formatted != code { code = formatted }
|
||||
canonicalCode = parseBadgeCode(formatted)
|
||||
}
|
||||
|
||||
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
let formatted = formatBadgeCodeInput(r.string)
|
||||
if parseBadgeCode(formatted) == nil {
|
||||
showAlert(
|
||||
NSLocalizedString("Invalid QR code", comment: "alert title"),
|
||||
message: NSLocalizedString("The code you scanned is not a badge code.", comment: "alert message")
|
||||
)
|
||||
} else {
|
||||
applyCodeInput(formatted)
|
||||
redeem()
|
||||
}
|
||||
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 {
|
||||
let (redeemedUser, newBadge) = try await apiRedeemBadgeCode(user.userId, sending)
|
||||
let badgeState = try? await apiGetBadgeState(user.userId)
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
// written before the pop: BadgesView swaps its content under this pushed view, so
|
||||
// the pop reveals Your Badge already in place rather than animating it afterwards
|
||||
if let badgeState {
|
||||
BadgeModel.shared.set(userId: user.userId, badgeState: badgeState)
|
||||
}
|
||||
// the response is the only carrier: redeeming raises no event that refreshes the
|
||||
// profile, so without this the badge beside the name is the one from before
|
||||
chatModel.updateUser(redeemedUser)
|
||||
if let badgeState, !badgeState.shown {
|
||||
// a replay adds no purchase; a fresh code's badge can be retired on arrival
|
||||
showAlert(NSLocalizedString("Cannot redeem code", comment: "alert title"), message: failureMessage(newBadge ? .badgeEnded : .codeUsed))
|
||||
} else {
|
||||
supporterBannerShown = true
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
let redeemError = error as? BadgeRedeemError ?? .unknown
|
||||
// the mapped case only - core embeds the service's response in some of these messages
|
||||
logger.error("apiRedeemBadgeCode: \(String(describing: redeemError))")
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
showAlert(NSLocalizedString("Cannot redeem code", comment: "alert title"), message: failureMessage(redeemError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func failureMessage(_ failure: BadgeRedeemError) -> String {
|
||||
switch failure {
|
||||
case .invalidCode: NSLocalizedString("This code is not valid.", comment: "alert message")
|
||||
case .serviceNotConfigured: NSLocalizedString("This app version cannot redeem badge codes.", comment: "alert message")
|
||||
case .alreadyActive: NSLocalizedString("This profile already has a badge. Redeem the code on another profile, or once this badge ends.", comment: "alert message")
|
||||
case .codeInvalid: NSLocalizedString("This code was not recognised.", comment: "alert message")
|
||||
case .codeUsed: NSLocalizedString("This code has already been used.", comment: "alert message")
|
||||
case .codeExpired: NSLocalizedString("This code has expired.", comment: "alert message")
|
||||
case .rateLimited: NSLocalizedString("Too many attempts. Please try again later.", comment: "alert message")
|
||||
case .serviceFailed: NSLocalizedString("The badge service is unavailable. Please try again later.", comment: "alert message")
|
||||
case .badServiceResponse: NSLocalizedString("The badge service sent an unexpected response.", comment: "alert message")
|
||||
case .credentialNotVerified: NSLocalizedString("This app version cannot verify this badge. Please update the app.", comment: "alert message")
|
||||
case .unsupportedVersion: NSLocalizedString("This app version is too old for the badge service. Please update the app.", comment: "alert message")
|
||||
case .networkError: NSLocalizedString("Connection error. Please check your network connection.", comment: "alert message")
|
||||
case .badgeEnded: NSLocalizedString("The code was accepted, but the badge it grants has already ended.", comment: "alert message")
|
||||
case .unknown: NSLocalizedString("The code could not be redeemed.", comment: "alert message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("SimpleX doesn't sell ads or data. It's funded by its users and by investors who share the mission. You can support the project and show a badge 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 private badges work").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,93 @@
|
||||
//
|
||||
// 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
|
||||
let badgeState: BadgeState
|
||||
var showsAsSheet: Bool = false
|
||||
|
||||
private var title: LocalizedStringKey {
|
||||
badgeState.shown ? "Your badge" : "Support ended"
|
||||
}
|
||||
|
||||
// pushed, the navigation bar carries the title and animates it; as a sheet root there is no bar
|
||||
// to put it in, so the title is drawn in the content, as the Support screen does
|
||||
private var navTitle: LocalizedStringKey { showsAsSheet ? "" : title }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if showsAsSheet {
|
||||
Text(title)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(navTitle)
|
||||
.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)
|
||||
|
||||
badgeTypeName(badgeState.badgeType)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("shown on your profile")
|
||||
.font(.footnote)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// verbatim for an unknown type: it is the service's string, and must not be looked up as a localised key
|
||||
private func badgeTypeName(_ t: BadgeType) -> Text {
|
||||
switch t {
|
||||
case .supporter: Text("Supporter")
|
||||
case .legend: Text("Legend")
|
||||
case .investor: Text("Investor")
|
||||
case let .unknown(s): Text(verbatim: s)
|
||||
}
|
||||
}
|
||||
@@ -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 private badges work").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 + files up to 5GB"
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ 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
|
||||
@@ -174,7 +175,9 @@ 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_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 +214,12 @@ struct ChatListView: View {
|
||||
NewChatSheet()
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
.appSheet(isPresented: $showBadgesSheet) {
|
||||
NavigationView {
|
||||
BadgesView(showsAsSheet: true)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.onChange(of: activeUserPickerSheet) {
|
||||
if $0 != nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
@@ -363,8 +372,38 @@ 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 func showSupportEndedDismissAlert() {
|
||||
showAlert(NSLocalizedString("Support ended", 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 {
|
||||
@@ -418,6 +457,30 @@ 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: "Support ended",
|
||||
subtitle: "Your support ended 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 && 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)
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
@@ -518,7 +581,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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -56,6 +56,7 @@ 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_TOOLBAR_MATERIAL = "toolbarMaterial"
|
||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||
@@ -117,6 +118,7 @@ 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_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
@@ -148,6 +150,7 @@ let hintDefaults = [
|
||||
DEFAULT_LA_NOTICE_SHOWN,
|
||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN,
|
||||
DEFAULT_ADDRESS_CREATION_CARD_SHOWN,
|
||||
DEFAULT_SUPPORTER_BANNER_SHOWN,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
|
||||
DEFAULT_SIGN_MESSAGE_ALERT_SHOWN,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
|
||||
@@ -341,6 +344,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 +394,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text(verbatim: "v\(appVersion ?? "?")")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -179,6 +179,14 @@
|
||||
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 */; };
|
||||
@@ -192,6 +200,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 +536,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>"; };
|
||||
@@ -559,6 +570,14 @@
|
||||
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>"; };
|
||||
@@ -573,6 +592,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>"; };
|
||||
@@ -772,6 +793,7 @@
|
||||
5C2E260D27A30E2400F70299 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64C03BF8302F423300072BDE /* Badges */,
|
||||
B76E6C2F2C5C41C300EC11AA /* Contacts */,
|
||||
5CB0BA8C282711BC00B3292C /* Onboarding */,
|
||||
3C714775281C080100CB4D4B /* Call */,
|
||||
@@ -892,6 +914,7 @@
|
||||
5CA059BD279559F40002BEB4 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
642E412B3033084000F04EE1 /* BadgeTestProducts.storekit */,
|
||||
E5C0BBFD2F82BBC000EA7527 /* Debug.xcconfig */,
|
||||
E5C0BBFE2F82BBC900EA7527 /* Release.xcconfig */,
|
||||
5C55A92D283D0FDE00C4E99E /* sounds */,
|
||||
@@ -1186,6 +1209,23 @@
|
||||
path = Group;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
64C03BF8302F423300072BDE /* Badges */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 = (
|
||||
@@ -1602,6 +1642,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 +1691,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 */,
|
||||
@@ -1689,6 +1738,7 @@
|
||||
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 */,
|
||||
|
||||
@@ -163,6 +163,14 @@ public func chatJsonLength(_ s: String) -> Int {
|
||||
return Int(chat_json_length(&c))
|
||||
}
|
||||
|
||||
// The code as core will accept it - prefix and 20 characters, no separators - or nil if it does not
|
||||
// parse. Validity is decided only here: a second check-character implementation would drift.
|
||||
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]?
|
||||
}
|
||||
|
||||
@@ -304,11 +304,108 @@ 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 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 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: Hashable {
|
||||
public var amount: Int64
|
||||
public var currency: String
|
||||
}
|
||||
|
||||
extension BadgeAlertPrice: Codable {
|
||||
// encoded as the Haskell tuple it comes from: [amount, currency]
|
||||
public init(from decoder: Decoder) throws {
|
||||
var c = try decoder.unkeyedContainer()
|
||||
amount = try c.decode(Int64.self)
|
||||
currency = try c.decode(String.self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var c = encoder.unkeyedContainer()
|
||||
try c.encode(amount)
|
||||
try c.encode(currency)
|
||||
}
|
||||
}
|
||||
|
||||
public enum BadgeAlertKind: Hashable {
|
||||
case renewalApproaching
|
||||
case paymentIssue
|
||||
case subscriptionEnded
|
||||
case prepaidEnding
|
||||
case supportEnded
|
||||
case unknown(String)
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .renewalApproaching: "renewal_approaching"
|
||||
case .paymentIssue: "payment_issue"
|
||||
case .subscriptionEnded: "subscription_ended"
|
||||
case .prepaidEnding: "prepaid_ending"
|
||||
case .supportEnded: "support_ended"
|
||||
case let .unknown(s): s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BadgeAlertKind: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
switch try decoder.singleValueContainer().decode(String.self) {
|
||||
case "renewal_approaching": self = .renewalApproaching
|
||||
case "payment_issue": self = .paymentIssue
|
||||
case "subscription_ended": self = .subscriptionEnded
|
||||
case "prepaid_ending": self = .prepaidEnding
|
||||
case "support_ended": self = .supportEnded
|
||||
case let s: self = .unknown(s)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var c = encoder.singleValueContainer()
|
||||
try c.encode(text)
|
||||
}
|
||||
}
|
||||
|
||||
// the wire proof carried on a profile - opaque to the UI, only round-tripped back to the core (apiPrepareContact)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+110
@@ -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,88 @@ 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 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 BadgeAlert(
|
||||
val kind: BadgeAlertKind,
|
||||
val episode: String,
|
||||
val date: Instant,
|
||||
val price: BadgeAlertPrice? = null
|
||||
) {
|
||||
val dateText: String get() = badgeDateText(date)
|
||||
}
|
||||
|
||||
@Serializable(with = BadgeAlertPriceSerializer::class)
|
||||
data class BadgeAlertPrice(val amount: Long, val currency: String)
|
||||
|
||||
// encoded as the Haskell tuple it comes from: [amount, currency]
|
||||
object BadgeAlertPriceSerializer : KSerializer<BadgeAlertPrice> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BadgeAlertPrice")
|
||||
override fun deserialize(decoder: Decoder): BadgeAlertPrice {
|
||||
require(decoder is JsonDecoder)
|
||||
val arr = decoder.decodeJsonElement().jsonArray
|
||||
return BadgeAlertPrice(arr[0].jsonPrimitive.long, arr[1].jsonPrimitive.content)
|
||||
}
|
||||
override fun serialize(encoder: Encoder, value: BadgeAlertPrice) {
|
||||
require(encoder is JsonEncoder)
|
||||
encoder.encodeJsonElement(buildJsonArray { add(value.amount); add(value.currency) })
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = BadgeAlertKindSerializer::class)
|
||||
sealed class BadgeAlertKind {
|
||||
@Serializable @SerialName("renewal_approaching") object RenewalApproaching: BadgeAlertKind()
|
||||
@Serializable @SerialName("payment_issue") object PaymentIssue: BadgeAlertKind()
|
||||
@Serializable @SerialName("subscription_ended") object SubscriptionEnded: BadgeAlertKind()
|
||||
@Serializable @SerialName("prepaid_ending") object PrepaidEnding: BadgeAlertKind()
|
||||
@Serializable @SerialName("support_ended") object SupportEnded: BadgeAlertKind()
|
||||
@Serializable @SerialName("unknown") data class Unknown(val kind: String): BadgeAlertKind()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is RenewalApproaching -> "renewal_approaching"
|
||||
is PaymentIssue -> "payment_issue"
|
||||
is SubscriptionEnded -> "subscription_ended"
|
||||
is PrepaidEnding -> "prepaid_ending"
|
||||
is SupportEnded -> "support_ended"
|
||||
is Unknown -> kind
|
||||
}
|
||||
}
|
||||
|
||||
object BadgeAlertKindSerializer : KSerializer<BadgeAlertKind> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BadgeAlertKind", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): BadgeAlertKind =
|
||||
when (val v = decoder.decodeString()) {
|
||||
"renewal_approaching" -> BadgeAlertKind.RenewalApproaching
|
||||
"payment_issue" -> BadgeAlertKind.PaymentIssue
|
||||
"subscription_ended" -> BadgeAlertKind.SubscriptionEnded
|
||||
"prepaid_ending" -> BadgeAlertKind.PrepaidEnding
|
||||
"support_ended" -> BadgeAlertKind.SupportEnded
|
||||
else -> BadgeAlertKind.Unknown(v)
|
||||
}
|
||||
override fun serialize(encoder: Encoder, value: BadgeAlertKind) = encoder.encodeString(value.text)
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
+169
@@ -192,6 +192,7 @@ 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 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 +274,7 @@ class AppPreferences {
|
||||
hintPref(laNoticeShown, false),
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(supporterBannerShown, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
@@ -464,6 +466,7 @@ 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_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 +562,94 @@ object ChatController {
|
||||
|
||||
suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats())
|
||||
|
||||
fun badgeRedeemError(error: ChatError): BadgeRedeemError {
|
||||
// the app's own classifier decides what counts as a network failure; only the classification is
|
||||
// used here, not its retry policy - a client that retries makes code guessing cheaper
|
||||
if (retryableNetworkErrorAlert(error) != null) return BadgeRedeemError.NetworkError
|
||||
if (error !is ChatError.ChatErrorChat || error.errorType !is ChatErrorType.CommandError) return BadgeRedeemError.Unknown
|
||||
val message = error.errorType.message
|
||||
when (message) {
|
||||
BadgeErrorText.invalidCode -> return BadgeRedeemError.InvalidCode
|
||||
BadgeErrorText.serviceNotConfigured -> return BadgeRedeemError.ServiceNotConfigured
|
||||
BadgeErrorText.alreadyActive -> return BadgeRedeemError.AlreadyActive
|
||||
BadgeErrorText.unknownKeyIndex, BadgeErrorText.credentialNotVerified -> return BadgeRedeemError.CredentialNotVerified
|
||||
BadgeErrorText.undecodableResponse -> return BadgeRedeemError.BadServiceResponse
|
||||
}
|
||||
if (message.startsWith(BadgeErrorText.invalidResponse) || message.startsWith(BadgeErrorText.unexpectedResponse)) {
|
||||
return BadgeRedeemError.BadServiceResponse
|
||||
}
|
||||
if (message.startsWith(BadgeErrorText.serviceError)) {
|
||||
return badgeServiceError(message.removePrefix(BadgeErrorText.serviceError))
|
||||
}
|
||||
return BadgeRedeemError.Unknown
|
||||
}
|
||||
|
||||
// the service's own tag, which core bounds to [a-z0-9_] and 32 characters and which is never shown
|
||||
private fun badgeServiceError(tag: String): BadgeRedeemError = when (tag) {
|
||||
"code_invalid" -> BadgeRedeemError.CodeInvalid
|
||||
"code_used" -> BadgeRedeemError.CodeUsed
|
||||
"code_expired" -> BadgeRedeemError.CodeExpired
|
||||
"rate_limited" -> BadgeRedeemError.RateLimited
|
||||
// retrying never succeeds: the client is too old for the service
|
||||
"unsupported_version" -> BadgeRedeemError.UnsupportedVersion
|
||||
else -> BadgeRedeemError.ServiceFailed
|
||||
}
|
||||
|
||||
// log = false because the code is a bearer secret until it is redeemed - it is in the command, and a
|
||||
// service response echoed into an error message would carry it into the terminal with the response.
|
||||
suspend fun apiRedeemBadgeCode(rh: Long?, userId: Long, code: String): BadgeRedeemResult {
|
||||
val r = sendCmd(rh, CC.ApiRedeemBadgeCode(userId, code), log = false)
|
||||
return when {
|
||||
// redeemedBadge is dropped: it is the credential, and the user's profile carries what is shown
|
||||
r is API.Result && r.res is CR.BadgeRedeemed -> BadgeRedeemResult.Redeemed(r.res.user, r.res.newBadge)
|
||||
r is API.Error -> BadgeRedeemResult.Failed(badgeRedeemError(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(BadgeRedeemError.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 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 +771,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
|
||||
@@ -3472,6 +3564,20 @@ object ChatController {
|
||||
chatModel.chatsContext.updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.BadgeChanged ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// read by core after retiring or presenting, so it carries the profile badge as changed
|
||||
chatModel.updateUser(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 +3875,43 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// The failures redeemBadgeCode raises, as core flattens them to command error text.
|
||||
// tests/Bots/BadgeServiceTests.hs asserts that text, so a change in core breaks a test there.
|
||||
enum class BadgeRedeemError {
|
||||
InvalidCode,
|
||||
ServiceNotConfigured,
|
||||
AlreadyActive,
|
||||
CodeInvalid,
|
||||
CodeUsed,
|
||||
CodeExpired,
|
||||
RateLimited,
|
||||
ServiceFailed,
|
||||
BadServiceResponse,
|
||||
CredentialNotVerified,
|
||||
UnsupportedVersion,
|
||||
NetworkError,
|
||||
BadgeEnded,
|
||||
Unknown
|
||||
}
|
||||
|
||||
private object BadgeErrorText {
|
||||
const val invalidCode = "invalid badge code"
|
||||
const val serviceNotConfigured = "badge service not configured"
|
||||
const val alreadyActive = "badge already active"
|
||||
const val unknownKeyIndex = "unknown badge key index"
|
||||
const val credentialNotVerified = "badge credential does not verify against configured key"
|
||||
const val invalidResponse = "invalid badge service response, "
|
||||
const val unexpectedResponse = "unexpected badge service response: "
|
||||
const val serviceError = "badge service error: "
|
||||
// raised by sendServiceRequestTo, not by redeemBadgeCode itself, when the reply is not JSON
|
||||
const val undecodableResponse = "invalid service response"
|
||||
}
|
||||
|
||||
sealed class BadgeRedeemResult {
|
||||
class Redeemed(val user: User, val newBadge: Boolean): BadgeRedeemResult()
|
||||
class Failed(val error: BadgeRedeemError): BadgeRedeemResult()
|
||||
}
|
||||
|
||||
// ChatCommand
|
||||
// Spec: spec/api.md#CC
|
||||
sealed class CC {
|
||||
@@ -3947,6 +4090,10 @@ 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 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 +4315,9 @@ 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 ApiAckBadgeAlert -> "/_badge ack $userId $badgePurchaseId ${alertKind.text} ${onOff(snooze)} $episode"
|
||||
is ShowVersion -> "/version"
|
||||
is ResetAgentServersStats -> "/reset servers stats"
|
||||
is GetAgentSubsTotal -> "/get subs total $userId"
|
||||
@@ -4348,6 +4498,9 @@ sealed class CC {
|
||||
is ApiUploadStandaloneFile -> "apiUploadStandaloneFile"
|
||||
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
|
||||
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
|
||||
is ApiRedeemBadgeCode -> "apiRedeemBadgeCode"
|
||||
is ApiGetBadgeState -> "apiGetBadgeState"
|
||||
is ApiAckBadgeAlert -> "apiAckBadgeAlert"
|
||||
is ShowVersion -> "showVersion"
|
||||
is ResetAgentServersStats -> "resetAgentServersStats"
|
||||
is GetAgentSubsTotal -> "getAgentSubsTotal"
|
||||
@@ -4369,6 +4522,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
|
||||
}
|
||||
|
||||
@@ -6722,6 +6877,12 @@ 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): CR()
|
||||
@Serializable @SerialName("badgeState") class BadgeStateR(val user: UserRef, val badgeState: BadgeState?): 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 +7069,10 @@ sealed class CR {
|
||||
is ArchiveExported -> "archiveExported"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is AppSettingsR -> "appSettings"
|
||||
is BadgeRedeemed -> "badgeRedeemed"
|
||||
is BadgeStateR -> "badgeState"
|
||||
is BadgeChanged -> "badgeChanged"
|
||||
is BadgeAlertR -> "badgeAlert"
|
||||
is Response -> "* $type"
|
||||
is Invalid -> "* invalid json"
|
||||
}
|
||||
@@ -7111,6 +7276,10 @@ 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")
|
||||
is BadgeStateR -> withUser(user, json.encodeToString(badgeState))
|
||||
is BadgeChanged -> withUser(user, json.encodeToString(badgeState))
|
||||
is BadgeAlertR -> withUser(user, json.encodeToString(badgeAlert))
|
||||
is Response -> json
|
||||
is Invalid -> str
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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>
|
||||
|
||||
+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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
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.res.MR
|
||||
|
||||
// TODO [badges]: replace lorem ipsum with the real copy once the badge protocol and privacy properties are documented.
|
||||
@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)
|
||||
}
|
||||
}
|
||||
+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)
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
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.StringResource
|
||||
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
|
||||
|
||||
// The code as core will accept it - prefix and 20 characters, no separators - or null if it does not
|
||||
// parse. Validity is decided only here: a second check-character implementation would drift.
|
||||
fun parseBadgeCode(s: String): String? {
|
||||
val canonical = chatParseBadgeCode(s)
|
||||
return if (canonical.isEmpty()) null else canonical
|
||||
}
|
||||
|
||||
// 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) }
|
||||
|
||||
fun applyCodeInput(s: String) {
|
||||
val formatted = formatBadgeCodeInput(s)
|
||||
if (formatted != code.value.text) code.value = TextFieldValue(formatted, selection = TextRange(formatted.length))
|
||||
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)) {
|
||||
is BadgeRedeemResult.Redeemed -> {
|
||||
val badgeState = try { chatModel.controller.apiGetBadgeState(rhId, user.userId) } catch (e: Exception) { null }
|
||||
withContext(Dispatchers.Main) {
|
||||
submitting.value = false
|
||||
// written before the pop: BadgesView swaps its content under this pushed view, so
|
||||
// the pop reveals Your Badge already in place rather than animating it afterwards
|
||||
if (badgeState != null) {
|
||||
BadgeModel.set(rhId, user.userId, badgeState)
|
||||
}
|
||||
// the response is the only carrier: redeeming raises no event that refreshes the
|
||||
// profile, so without this the badge beside the name is the one from before
|
||||
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(failureMessage(if (result.newBadge) BadgeRedeemError.BadgeEnded else BadgeRedeemError.CodeUsed))
|
||||
)
|
||||
} else {
|
||||
supporterBannerShown.set(true)
|
||||
ModalManager.start.closeModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
is BadgeRedeemResult.Failed -> {
|
||||
// the mapped case only - core embeds the service's response in some of these messages
|
||||
Log.e(TAG, "apiRedeemBadgeCode: ${result.error}")
|
||||
withContext(Dispatchers.Main) {
|
||||
submitting.value = false
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_error_title),
|
||||
text = generalGetString(failureMessage(result.error))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(formatted)
|
||||
redeem()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SubmitButton(enabled = canonicalCode.value != null && !submitting.value, onClick = ::redeem)
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CodeField(code: MutableState<TextFieldValue>, submitting: Boolean, applyCodeInput: (String) -> 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(it.text) },
|
||||
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: (String) -> Unit) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
TextButton(
|
||||
onClick = { clipboard.getText()?.text?.let { applyCodeInput(it) } },
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private fun failureMessage(failure: BadgeRedeemError): StringResource = when (failure) {
|
||||
BadgeRedeemError.InvalidCode -> MR.strings.badges_error_invalid_code
|
||||
BadgeRedeemError.ServiceNotConfigured -> MR.strings.badges_error_service_not_configured
|
||||
BadgeRedeemError.AlreadyActive -> MR.strings.badges_error_already_active
|
||||
BadgeRedeemError.CodeInvalid -> MR.strings.badges_error_code_invalid
|
||||
BadgeRedeemError.CodeUsed -> MR.strings.badges_error_code_used
|
||||
BadgeRedeemError.CodeExpired -> MR.strings.badges_error_code_expired
|
||||
BadgeRedeemError.RateLimited -> MR.strings.badges_error_rate_limited
|
||||
BadgeRedeemError.ServiceFailed -> MR.strings.badges_error_service_failed
|
||||
BadgeRedeemError.BadServiceResponse -> MR.strings.badges_error_bad_service_response
|
||||
BadgeRedeemError.CredentialNotVerified -> MR.strings.badges_error_credential_not_verified
|
||||
BadgeRedeemError.UnsupportedVersion -> MR.strings.badges_error_unsupported_version
|
||||
BadgeRedeemError.NetworkError -> MR.strings.badges_error_network
|
||||
BadgeRedeemError.BadgeEnded -> MR.strings.badges_error_badge_ended
|
||||
BadgeRedeemError.Unknown -> MR.strings.badges_error_unknown
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
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
|
||||
if (platform.androidHasPlatformStore) {
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
} else {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun BadgesView() {
|
||||
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
|
||||
}
|
||||
|
||||
AnimatedContent(targetState = shownBadge, transitionSpec = { fadeIn() with fadeOut() }) { badgeState ->
|
||||
if (badgeState != null) {
|
||||
BadgesYourBadgeView(badgeState)
|
||||
} else {
|
||||
BadgesSupportSimplexView()
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
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.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.BadgeType
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.badgeImage
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgesYourBadgeView(badgeState: BadgeState) {
|
||||
val title = if (badgeState.shown) MR.strings.badges_your_badge else MR.strings.badges_support_ended
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(title))
|
||||
|
||||
SectionView {
|
||||
BadgeSummary(badgeState)
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.badges_ends).uppercase()) {
|
||||
Text(badgeState.paidThroughText, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 12.dp))
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.badges_prepaid_footer))
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgeSummary(badgeState: BadgeState) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Image(
|
||||
painterResource(badgeImage(badgeState.badgeType)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(68.dp).padding(bottom = 8.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// verbatim for an unknown type: it is the service's string, and must not be looked up as a localised key
|
||||
@Composable
|
||||
private fun badgeTypeName(t: BadgeType): String = when (t) {
|
||||
is BadgeType.Supporter -> stringResource(MR.strings.badges_level_supporter)
|
||||
is BadgeType.Legend -> stringResource(MR.strings.badges_level_legend)
|
||||
is BadgeType.Investor -> stringResource(MR.strings.badges_type_investor)
|
||||
is BadgeType.Unknown -> t.type
|
||||
}
|
||||
+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)
|
||||
)
|
||||
}
|
||||
+61
@@ -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_dont_show_again), 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,7 @@ 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 activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
@@ -1000,6 +1038,29 @@ 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.showModal { BadgesView() } },
|
||||
onDismiss = ::showSupportEndedDismissAlert
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (!supporterBannerShown.value && chatModel.chats.value.size > 3) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
onTap = { ModalManager.start.showModal { BadgesView() } },
|
||||
onDismiss = ::showSupportSimpleXDismissAlert
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
+1
-1
@@ -244,7 +244,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
|
||||
|
||||
+6
-1
@@ -42,13 +42,18 @@ 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 hasConversations(chats: List<Chat>): Boolean =
|
||||
chats.any { chat ->
|
||||
when (val c = chat.chatInfo) {
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+48
@@ -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) {
|
||||
|
||||
+13
@@ -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.showModal { BadgesView() } }) {
|
||||
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) {
|
||||
|
||||
@@ -1754,12 +1754,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 +1773,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>
|
||||
@@ -1962,9 +1959,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 +2102,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 +2113,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 +2142,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 +2160,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>
|
||||
@@ -2207,7 +2194,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 +2273,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 +2297,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 +2422,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 +2477,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 +2508,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 +2543,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 +2570,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 +2589,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 +2757,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 +2791,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 +2807,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 +2822,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 +2836,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>
|
||||
|
||||
@@ -203,11 +203,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 +368,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 +418,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>
|
||||
@@ -528,7 +521,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>
|
||||
@@ -726,8 +718,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>
|
||||
@@ -880,7 +870,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>
|
||||
@@ -987,7 +976,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>
|
||||
@@ -1154,7 +1142,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>
|
||||
@@ -1162,7 +1149,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>
|
||||
@@ -1229,7 +1215,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>
|
||||
@@ -1432,13 +1417,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 -->
|
||||
@@ -1579,7 +1559,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>
|
||||
@@ -1626,7 +1605,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>
|
||||
@@ -1660,7 +1638,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>
|
||||
@@ -1794,7 +1771,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>
|
||||
@@ -2037,7 +2013,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 -->
|
||||
@@ -2208,7 +2183,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>
|
||||
@@ -2714,7 +2688,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>
|
||||
@@ -3175,7 +3148,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>
|
||||
@@ -3193,12 +3165,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>
|
||||
@@ -3229,4 +3199,76 @@
|
||||
<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">SimpleX doesn\'t sell ads or data. It\'s funded by its users and by investors who share the mission. You can support the project and show a badge 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 private badges work</string>
|
||||
<string name="badges_how_it_works_title">How private badges work</string>
|
||||
<string name="badges_how_it_works_p1">A badge is not an account. It is a signed credential stored on your device. It does not identify you, and no one keeps a record of who holds which badge.</string>
|
||||
<string name="badges_how_it_works_p2">Your contacts see the badge and its expiry date, and nothing else. The badge carries no identifier, so it cannot be used to find out who you are or to match you across chats.</string>
|
||||
<string name="badges_how_it_works_p3">Payment and badge are kept apart. Paying is one step; the badge is issued in another, under a key that exists only for that badge. Whoever handles the payment cannot see where the badge ends up.</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 + files up to 5GB</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 from your receipt.</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_service_failed">The badge service is unavailable. Please try again later.</string>
|
||||
<string name="badges_error_bad_service_response">The badge service sent an unexpected response.</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_network">Connection error. Please check your network connection.</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">Support ended</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 support ended on %1$s.</string>
|
||||
<string name="badges_remind_me_later">Remind me later</string>
|
||||
<string name="badges_dont_show_again">Don\'t show again</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>
|
||||
@@ -1780,7 +1780,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 +1832,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>
|
||||
@@ -2053,11 +2051,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>
|
||||
@@ -2125,8 +2121,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 +2134,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 +2151,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>
|
||||
@@ -2179,9 +2169,7 @@
|
||||
<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 +2183,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 +2254,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 +2441,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 +2459,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 +2491,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>
|
||||
|
||||
@@ -493,7 +493,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>
|
||||
@@ -740,7 +739,6 @@
|
||||
<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>
|
||||
@@ -1006,7 +1004,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 +1087,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>
|
||||
@@ -1226,9 +1222,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>
|
||||
@@ -1307,7 +1300,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>
|
||||
@@ -1746,7 +1738,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>
|
||||
@@ -1905,8 +1896,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>
|
||||
@@ -1936,7 +1925,6 @@
|
||||
<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>
|
||||
@@ -1978,7 +1966,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 +2122,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>
|
||||
@@ -2262,7 +2248,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 +2285,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 +2411,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 +2465,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>
|
||||
|
||||
@@ -1781,8 +1781,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>
|
||||
@@ -1937,7 +1935,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 +1946,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>
|
||||
@@ -2058,7 +2054,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 +2139,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,7 +2185,6 @@
|
||||
<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>
|
||||
@@ -2206,7 +2199,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 +2214,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 +2262,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 +2288,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 +2304,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 +2480,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 +2494,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 +2514,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 +2563,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 +2617,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 +2636,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 +2687,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 +2753,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>
|
||||
|
||||
@@ -438,7 +438,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>
|
||||
@@ -547,7 +546,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 +677,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>
|
||||
@@ -874,7 +870,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 +889,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 +919,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>
|
||||
|
||||
@@ -1848,11 +1848,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 +1891,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>
|
||||
@@ -2058,9 +2055,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 +2204,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 +2215,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 +2256,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,7 +2267,6 @@
|
||||
<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>
|
||||
@@ -2372,7 +2358,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 +2394,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 +2518,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 +2573,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 +2602,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 +2631,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 +2660,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 +2676,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 +2854,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 +2890,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 +2900,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 +2911,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 +2919,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 +2933,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>
|
||||
|
||||
@@ -543,7 +543,6 @@
|
||||
<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>
|
||||
@@ -560,7 +559,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>
|
||||
@@ -925,7 +923,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>
|
||||
@@ -1216,8 +1213,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 +1284,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>
|
||||
@@ -1341,7 +1335,6 @@
|
||||
<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>
|
||||
@@ -1375,7 +1368,6 @@
|
||||
<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>
|
||||
@@ -1438,11 +1430,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>
|
||||
@@ -1737,7 +1727,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>
|
||||
@@ -1955,7 +1944,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 +2013,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>
|
||||
@@ -2088,7 +2075,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 +2126,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 +2189,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>
|
||||
@@ -2377,7 +2361,6 @@
|
||||
<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>
|
||||
@@ -2437,7 +2420,6 @@
|
||||
<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>
|
||||
@@ -2513,7 +2495,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>
|
||||
|
||||
@@ -1761,8 +1761,6 @@
|
||||
<string name="network_smp_proxy_mode_private_routing">Enrutamiento privado</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Servidores desconocidos</string>
|
||||
<string name="network_smp_proxy_mode_never_description">NO usar enrutamiento privado.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Enrutamiento de mensajes alternativo</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Modo de enrutamiento de mensajes</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">No</string>
|
||||
<string name="private_routing_show_message_status">Estado del mensaje</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida.</string>
|
||||
@@ -1816,7 +1814,6 @@
|
||||
<string name="color_primary_variant2">Color adicional 2</string>
|
||||
<string name="wallpaper_advanced_settings">Configuración avanzada</string>
|
||||
<string name="settings_section_title_user_theme">Tema del perfil</string>
|
||||
<string name="chat_list_always_visible">Listado del chat en ventana nueva</string>
|
||||
<string name="color_wallpaper_tint">Color imagen de fondo</string>
|
||||
<string name="message_queue_info_server_info">información cola del servidor: %1$s
|
||||
\n
|
||||
@@ -1984,13 +1981,11 @@
|
||||
<string name="privacy_media_blur_radius_soft">Suave</string>
|
||||
<string name="one_hand_ui">Barra de herramientas accesible</string>
|
||||
<string name="info_view_call_button">llamar</string>
|
||||
<string name="info_view_connect_button">conectar</string>
|
||||
<string name="delete_members_messages__question">¿Eliminar %d mensajes de miembros?</string>
|
||||
<string name="info_view_message_button">mensaje</string>
|
||||
<string name="compose_message_placeholder">Mensaje</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nada seleccionado</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Los mensajes se marcarán para eliminar. El destinatario o destinatarios podrán revelar estos mensajes.</string>
|
||||
<string name="info_view_open_button">abrir</string>
|
||||
<string name="select_verb">Seleccionar</string>
|
||||
<string name="selected_chat_items_selected_n">Seleccionados %d</string>
|
||||
<string name="toolbar_settings">Configuración</string>
|
||||
@@ -2123,8 +2118,6 @@
|
||||
<string name="simplex_address_or_1_time_link">¿Dirección SimpleX o enlace de un solo uso?</string>
|
||||
<string name="onboarding_choose_server_operators">Operadores de servidores</string>
|
||||
<string name="onboarding_network_operators">Operadores de red</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Las condiciones de los operadores habilitados serán aceptadas después de 30 días.</string>
|
||||
<string name="onboarding_network_operators_review_later">Revisar más tarde</string>
|
||||
<string name="operator_conditions_accepted_on">Condiciones aceptadas el: %s.</string>
|
||||
<string name="operator_info_title">Operador de red</string>
|
||||
<string name="operator">Operador</string>
|
||||
@@ -2156,7 +2149,6 @@
|
||||
<string name="v6_2_network_decentralization_enable_flux">Activa Flux en Servidores y Red para mejorar la privacidad de los metadatos.</string>
|
||||
<string name="error_accepting_operator_conditions">Error al aceptar las condiciones</string>
|
||||
<string name="error_updating_server_title">Error al actualizar el servidor</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">para mejorar la privacidad de los metadatos.</string>
|
||||
<string name="message_deleted_or_not_received_error_title">Ningún mensaje</string>
|
||||
<string name="smp_servers_new_server">Servidor nuevo</string>
|
||||
<string name="no_media_servers_configured">Sin servidores para archivos y multimedia.</string>
|
||||
@@ -2164,11 +2156,8 @@
|
||||
<string name="no_message_servers_configured_for_receiving">Sin servidores para recibir mensajes.</string>
|
||||
<string name="operator_server_alert_title">Servidor del operador</string>
|
||||
<string name="or_to_share_privately">O para compartir en privado</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Selecciona operadores a usar.</string>
|
||||
<string name="share_address_publicly">Compartir dirección públicamente</string>
|
||||
<string name="simplex_address_and_1_time_links_are_safe_to_share">Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio.</string>
|
||||
<string name="onboarding_network_operators_update">Actualizar</string>
|
||||
<string name="operator_website">Sitio web</string>
|
||||
<string name="your_servers">Tus servidores</string>
|
||||
<string name="use_servers_of_operator_x">Usar %s</string>
|
||||
<string name="operator_use_operator_toggle_description">Usar servidores</string>
|
||||
@@ -2183,7 +2172,6 @@
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Los enlaces de un uso pueden ser usados <i>solamente con un contacto</i> - comparte en persona o mediante cualquier aplicación de mensajería.]]></string>
|
||||
<string name="you_can_set_connection_name_to_remember">Puedes añadir un nombre a la conexión para recordar a quién corresponde.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Puedes configurar los operadores desde los ajustes de Servidores y Red.</string>
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[Las condiciones se han aceptado para el(los) operador(s): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Las condiciones serán aceptadas para el/los operador(es): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): <b>%s</b>.]]></string>
|
||||
@@ -2198,10 +2186,8 @@
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Las mismas condiciones se aplican a el/los operador(es) <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_applied"><![CDATA[Estas condiciones también se aplican para: <b>%s</b>.]]></string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Por ejemplo, si tu contacto recibe a través de un servidor de SimpleX Chat, tu aplicación enviará a través de un servidor de Flux.</string>
|
||||
<string name="address_creation_instruction">Pulsa Crear dirección SimpleX en el menú para crearla más tarde.</string>
|
||||
<string name="connection_error_quota_desc">La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">El mensaje ha sido eliminado o aún no se ha recibido.</string>
|
||||
<string name="remote_hosts_section">Móvil remoto</string>
|
||||
<string name="chat_archive">O importa desde un archivo</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Mensajes directos entre miembros de este chat no permitidos.</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>En dispositivos Xiaomi:</b> por favor, habilita el Autoinicio en los ajustes del sistema para que las notificaciones funcionen.]]></string>
|
||||
@@ -2281,7 +2267,6 @@
|
||||
<string name="prefs_error_saving_settings">Error al guardar la configuración</string>
|
||||
<string name="report_item_archived_by">informe archivado por %s</string>
|
||||
<string name="report_archive_alert_title">¿Archivar informe?</string>
|
||||
<string name="report_archive_alert_desc">El informe será archivado para ti.</string>
|
||||
<string name="report_verb">Informar</string>
|
||||
<string name="delete_report">Eliminar informe</string>
|
||||
<string name="group_reports_active_one">1 informe</string>
|
||||
@@ -2329,7 +2314,6 @@
|
||||
<string name="network_smp_web_port_footer">Se usa el puerto TCP %1$s cuando no se ha especificado otro.</string>
|
||||
<string name="network_smp_web_port_toggle">Usar puerto web</string>
|
||||
<string name="mute_all_chat">Silenciar todo</string>
|
||||
<string name="unread_mentions">Menciones sin leer</string>
|
||||
<string name="max_group_mentions_per_message_reached">¡Puedes mencionar hasta %1$s miembros por mensaje!</string>
|
||||
<string name="group_members_can_send_reports">Los miembros pueden informar de mensajes a los moderadores.</string>
|
||||
<string name="disable_sending_member_reports">No se permite informar de mensajes a los moderadores.</string>
|
||||
@@ -2466,7 +2450,6 @@
|
||||
<string name="compose_view_send_request">Enviar solicitud</string>
|
||||
<string name="compose_view_send_request_without_message">Enviar solicitud sin mensaje</string>
|
||||
<string name="v6_4_support_chat_descr">Envía tu comentario privado a los grupos.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Enviadp a tu contacto tras la conexión.</string>
|
||||
<string name="share_group_profile_via_link">¿Actualizar enlace de grupo?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Actualizar</string>
|
||||
<string name="share_profile_via_link">¿Actualizar la dirección?</string>
|
||||
@@ -2510,7 +2493,6 @@
|
||||
<string name="upgrade_group_link">Actualizar enlace de grupo</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Solicitudes de contacto en grupos</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">conexión solicitada desde el grupo %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Esta configuración se aplica al perfil actual</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Miembro eliminado, no puede aceptar solicitudes</string>
|
||||
<string name="allow_files_and_media_only_if">Se permiten archivos y multimedia pero sólo si tu contacto también los permite.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Permes que tus contactos envíen archivos y multimedia.</string>
|
||||
@@ -2541,7 +2523,6 @@
|
||||
<string name="remove_member_delete_messages_confirmation">Eliminar miembro y sus mensajes</string>
|
||||
<string name="content_filter_all_messages">Todos los mensajes</string>
|
||||
<string name="content_filter_files">Archivos</string>
|
||||
<string name="content_filter_menu_item">Filtro</string>
|
||||
<string name="content_filter_images">Imágenes</string>
|
||||
<string name="content_filter_links">Enlaces</string>
|
||||
<string name="placeholder_search_files">Buscar archivos</string>
|
||||
@@ -2592,7 +2573,6 @@
|
||||
<string name="info_row_channel">Canal</string>
|
||||
<string name="channel_full_name_field">Título completo:</string>
|
||||
<string name="channel_link">Enlace del canal</string>
|
||||
<string name="button_channel_members">Miembros canal</string>
|
||||
<string name="channel_display_name_field">Título del canal</string>
|
||||
<string name="snd_channel_event_channel_profile_updated">perfil del canal actualizado</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">El canal será eliminado para todos los suscriptores. ¡No puede deshacerse!</string>
|
||||
@@ -2608,7 +2588,6 @@
|
||||
<string name="rcv_msg_error_dropped">caído (%1$d intentos)</string>
|
||||
<string name="button_edit_channel_profile">Editar perfil del canal</string>
|
||||
<string name="error_creating_channel">Error al crear el canal</string>
|
||||
<string name="error_opening_channel">Error al abrir el canal</string>
|
||||
<string name="rcv_msg_error_parse">error:%s</string>
|
||||
<string name="error_saving_channel_profile">Error al guardar el perfil del canal</string>
|
||||
<string name="relay_conn_status_failed">fallo</string>
|
||||
@@ -2629,7 +2608,6 @@
|
||||
<string name="no_chat_relays_enabled">Ningún servidor de chat activado.</string>
|
||||
<string name="not_all_relays_connected">Hay servidores no conectados</string>
|
||||
<string name="connect_plan_open_channel">Abrir canal</string>
|
||||
<string name="connect_plan_open_new_channel">Abrir canal nuevo</string>
|
||||
<string name="member_info_section_title_owner">Propietario</string>
|
||||
<string name="channel_members_section_owners">Propietarios y colaboradores</string>
|
||||
<string name="preset_relay_address">Direcciones predefinidas</string>
|
||||
@@ -2804,7 +2782,6 @@
|
||||
<string name="link_previews_alert_desc_socks">Las previsualizaciones de enlaces se solicitan a través del proxy SOCKS. Las peticiónes DNS aún pueden usar el DNS local del sistema.</string>
|
||||
<string name="one_hand_ui_top_bar">Menú superior</string>
|
||||
<string name="cancel_channel_alert_msg">Tu nuevo canal %1$s está conectado a %2$d de %3$d servidores.\nSi cancelas, el canal será eliminado. Puedes crearlo de nuevo.</string>
|
||||
<string name="add_button">Añadir</string>
|
||||
<string name="add_relay_button">Añadir servidor</string>
|
||||
<string name="add_relays_title">Añadir servidores</string>
|
||||
<string name="button_cancel_and_delete_channel">Cancelar y eliminar el canal</string>
|
||||
@@ -2843,7 +2820,6 @@
|
||||
<string name="webpage_url_footer">Se mostrará a los suscriptores y se usará para permitir la carga de la vista previa.</string>
|
||||
<string name="more_privacy">Más privacidad</string>
|
||||
<string name="embed_only_your_page">Solo la página superior puede mostrar la vista previa.</string>
|
||||
<string name="please_upgrade_the_app">Por favor, actualiza la aplicación.</string>
|
||||
<string name="relay_status_rejected">rechazado</string>
|
||||
<string name="member_info_relay_status_rejected_by_operator">rechazado por el operador del servidor</string>
|
||||
<string name="badge_invested">%s ha participado en la financiación colectiva de SimpleX Chat.</string>
|
||||
@@ -2858,7 +2834,6 @@
|
||||
<string name="channel_owner_count_plural">%1$d propietarios</string>
|
||||
<string name="channel_owners_contributors_count">%1$d propietarios y colaboradores</string>
|
||||
<string name="badge_supported_simplex">%1$s ha apoyado a SimpleX Chat. La insignia caducó el %2$s.</string>
|
||||
<string name="settings_section_title_about">Acerca de</string>
|
||||
<string name="relay_status_acknowledged_roster">lista confirmada</string>
|
||||
<string name="webpage_code_footer">Añade este código a tu web. Mostrará una vista previa de tu canal o grupo.</string>
|
||||
<string name="advanced_options">Opciones avanzadas</string>
|
||||
@@ -2871,10 +2846,6 @@
|
||||
<string name="badge_unknown_key_title">No se pudo verificar la insignia</string>
|
||||
<string name="channel_webpage">Web del canal</string>
|
||||
<string name="chat_data">Datos del chat</string>
|
||||
<string name="unsupported_channel_name">Nombre de canal no compatible</string>
|
||||
<string name="unsupported_contact_name">Nombre de contacto no compatible</string>
|
||||
<string name="channel_name_requires_newer_app_version">Para conectarte mediante el nombre del canal es necesaria una versión más reciente de la aplicación.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Para conectarse mediante el nombre de un contacto es necesaria una versión más reciente de la aplicación.</string>
|
||||
<string name="group_link_requires_newer_version">Este grupo requiere una versión más reciente de la aplicación. Por favor, actualizala para unirte.</string>
|
||||
<string name="webpage_code">Código web</string>
|
||||
<string name="relays_no_web_support">Los servidores usados no admiten páginas web.</string>
|
||||
@@ -2892,7 +2863,6 @@
|
||||
<string name="simplex_name_resolver_error_desc">Error de resolución: %1$s</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">El servidor %1$s no admite la resolución de nombres. Configura un servidor, o usa un enlace para conectarte.</string>
|
||||
<string name="set_simplex_name">Escribe el nombre SimpleX</string>
|
||||
<string name="simplex_name">Nombre SimpleX</string>
|
||||
<string name="simplex_name_error">Error del nombre SimpleX</string>
|
||||
<string name="simplex_name_not_verified">Nombre SimpleX no verificado</string>
|
||||
<string name="simplex_name_no_valid_link_desc">El nombre SimpleX %1$s está registrado, pero no tiene un enlace válido.</string>
|
||||
|
||||
@@ -1576,10 +1576,8 @@
|
||||
<string name="network_smp_proxy_mode_never">هرگز</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">استفاده از مسیریابی خصوصی با سرورهای ناشناخته.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">استفاده از مسیریابی خصوصی با سرورهای ناشناخته وقتی نشانی IP محافظت نشده است.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">حالت مسیریابی پیام</string>
|
||||
<string name="network_smp_proxy_fallback_allow">بله</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">پیامها مستقیما فرستاده نشود، حتی اگر سرور مقصد شما از مسیریابی خصوصی پشتیبانی نکند.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">گزینه پسرفت مسیریابی پیام</string>
|
||||
<string name="private_routing_show_message_status">نمایش وضعیت پیام</string>
|
||||
<string name="private_routing_explanation">برای محافظت از نشانی IP شما، مسیریابی خصوصی از سرورهای SMP شما به منظور تحویل پیامها استفاده میکند.</string>
|
||||
<string name="settings_section_title_private_message_routing">مسیریابی پیام خصوصی</string>
|
||||
@@ -1797,7 +1795,6 @@
|
||||
<string name="message_queue_info_server_info">اطلاعات صف سرور: %1$s
|
||||
\n
|
||||
\nآخرین پیام دریافتی: %2$s</string>
|
||||
<string name="chat_list_always_visible">نمایش فهرست چت در پنجره جدید</string>
|
||||
<string name="chat_theme_apply_to_dark_mode">حالت تاریک</string>
|
||||
<string name="theme_black">سیاه</string>
|
||||
<string name="color_mode">حالت رنگ</string>
|
||||
@@ -1972,7 +1969,6 @@
|
||||
<string name="call_desktop_permission_denied_chrome">برای مجوز میکروفون، روی آیکون اطلاعات کنار نوار آدرس کلیک کنید</string>
|
||||
<string name="completed">تکمیل شد</string>
|
||||
<string name="confirm_delete_contact_question">تایید حذف مخاطب؟</string>
|
||||
<string name="info_view_connect_button">اتصال</string>
|
||||
<string name="compose_view_connect">اتصال</string>
|
||||
<string name="servers_info_sessions_connected">متصل</string>
|
||||
<string name="v6_4_connect_faster">اتصال سریع تر! 🚀</string>
|
||||
@@ -2049,7 +2045,6 @@
|
||||
<string name="group_member_status_rejected">رد شده</string>
|
||||
<string name="reject_pending_member_alert_title">رد کردن عضو؟</string>
|
||||
<string name="app_check_for_updates_button_remind_later">بعداً یادآوری کن</string>
|
||||
<string name="remote_hosts_section">موبایلهای از راه دور</string>
|
||||
<string name="migrate_from_device_remove_archive_question">حذف کردن آرشیو؟</string>
|
||||
<string name="cant_send_message_mem_removed">از گروه حذف شد</string>
|
||||
<string name="button_remove_members_question">اعضا حذف شوند؟</string>
|
||||
@@ -2075,7 +2070,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">بررسی اعضا قبل از پذیرش (کوبیدن).</string>
|
||||
<string name="save_admission_question">آیا تنظیمات پذیرش ذخیره شود؟</string>
|
||||
@@ -2089,7 +2083,6 @@
|
||||
<string name="select_chat_profile">انتخاب پروفایل چت</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="set_member_admission">تنظیم پذیرش اعضا</string>
|
||||
<string name="v6_3_set_message_expiration_in_chats">تنظیم انقضای پیام در چتها.</string>
|
||||
<string name="toolbar_settings">تنظیمات</string>
|
||||
@@ -2131,7 +2124,6 @@
|
||||
<string name="settings_message_shape_tail">انتها</string>
|
||||
<string name="chat_banner_connect_to_chat">برای چت، روی \"اتصال\" ضربه بزنید.</string>
|
||||
<string name="chat_banner_send_request_to_connect">برای ارسال درخواست، روی \"اتصال\" ضربه بزنید.</string>
|
||||
<string name="address_creation_instruction">برای ایجاد آدرس SimpleX در آینده، در منو روی \"ایجاد آدرس SimpleX\" ضربه بزنید.</string>
|
||||
<string name="chat_banner_join_group">روی \"پیوستن به گروه\" ضربه بزنید.</string>
|
||||
<string name="network_option_tcp_connection">اتصال TCP</string>
|
||||
<string name="network_option_tcp_connection_timeout_background">وقفه زمانی اتصال TCP در پسزمینه</string>
|
||||
@@ -2181,7 +2173,6 @@
|
||||
<string name="chunks_uploaded">بخشها بارگذاری شدند</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">شرایط برای اپراتورهای فعال پس از ۳۰ روز پذیرفته خواهد شد.</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>
|
||||
@@ -2307,7 +2298,6 @@
|
||||
<string name="network_smp_web_port_off">خاموش</string>
|
||||
<string name="only_chat_owners_can_change_prefs">فقط مالکین چت میتوانند تنظیمات را تغییر دهند.</string>
|
||||
<string name="only_delete_conversation">فقط گفتگو حذف شود</string>
|
||||
<string name="info_view_open_button">باز کردن</string>
|
||||
<string name="operator_open_changes">باز کردن تغییرات</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- باز کردن چت برای اولین پیام خوانده نشده.\n- پرش به پیامهای نقلقول شده.</string>
|
||||
<string name="operator_open_conditions">شرایط باز کردن</string>
|
||||
@@ -2354,7 +2344,6 @@
|
||||
<string name="sync_connection_question">اتصال اصلاح شود؟</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="onboarding_network_operators_app_will_use_for_routing">به عنوان مثال، اگر مخاطب شما پیامها را از طریق یک سرور چت SimpleX دریافت کند، برنامه شما آنها را از طریق یک سرور Flux تحویل خواهد داد.</string>
|
||||
<string name="report_archive_for_me">برای من</string>
|
||||
<string name="operator_use_for_messages_private_routing">برای مسیریابی خصوصی</string>
|
||||
@@ -2407,7 +2396,6 @@
|
||||
<string name="sent_directly">مستقیم ارسال شد</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">پیامهای ارسال شده</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>
|
||||
@@ -2418,7 +2406,6 @@
|
||||
<string name="servers_info">اطلاعات سرورها</string>
|
||||
<string name="servers_info_reset_stats_alert_message">آمار سرورها بازنشانی خواهد شد - این عمل غیرقابل بازگشت است!</string>
|
||||
<string name="text_field_set_chat_placeholder">نام چت را تنظیم کنید…</string>
|
||||
<string name="report_archive_alert_desc">گزارش برای شما بایگانی خواهد شد.</string>
|
||||
<string name="member_role_will_be_changed_with_notification_chat">نقش به %s تغییر خواهد کرد. همه در چت مطلع خواهند شد.</string>
|
||||
<string name="v6_2_network_decentralization_descr">دومین اپراتور پیشفرض در برنامه!</string>
|
||||
<string name="the_sender_will_not_be_notified">فرستنده مطلع نخواهد شد.</string>
|
||||
@@ -2437,8 +2424,6 @@
|
||||
<string name="appearance_in_app_bars_alpha">شفافیت</string>
|
||||
<string name="servers_info_transport_sessions_section_header">جلسات Transport</string>
|
||||
<string name="unblock_members_for_all_question">رفع انسداد اعضا برای همه؟</string>
|
||||
<string name="unread_mentions">منشنهای خوانده نشده</string>
|
||||
<string name="onboarding_network_operators_update">بهروزرسانی</string>
|
||||
<string name="app_check_for_updates_update_available">بهروزرسانی موجود است: %s</string>
|
||||
<string name="operator_updated_conditions">شرایط بهروزرسانی شده</string>
|
||||
<string name="app_check_for_updates_canceled">دانلود بهروزرسانی لغو شد</string>
|
||||
@@ -2461,7 +2446,6 @@
|
||||
<string name="info_view_video_button">ویدئو</string>
|
||||
<string name="view_conditions">مشاهده شرایط</string>
|
||||
<string name="view_updated_conditions">مشاهده شرایط بهروزرسانی شده</string>
|
||||
<string name="operator_website">وبسایت</string>
|
||||
<string name="address_welcome_message">پیام خوشآمدگویی</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">زمانی که بیش از یک اپراتور فعال است، هیچکدام از آنها فرادادهای برای فهمیدن اینکه چه کسی با چه کسی ارتباط برقرار میکند، ندارند.</string>
|
||||
<string name="xftp_server">سرور XFTP</string>
|
||||
@@ -2469,7 +2453,6 @@
|
||||
<string name="snd_group_event_member_accepted">شما این عضو را پذیرفتید</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">شما به این سرورها متصل نیستید. از مسیریابی خصوصی برای ارسال پیامها به آنها استفاده میشود.</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="max_group_mentions_per_message_reached">شما میتوانید در هر پیام تا %1$s عضو را منشن کنید!</string>
|
||||
@@ -2510,7 +2493,6 @@
|
||||
<string name="v6_4_1_new_interface_languages">۴ زبان جدید رابط کاربری</string>
|
||||
<string name="v6_4_1_new_interface_languages_descr">کاتالان، اندونزیایی، رومانیایی و ویتنامی - با تشکر از کاربران ما!</string>
|
||||
<string name="member_is_deleted_cant_accept_request">عضو حذف شده است - نمیتوان درخواست را قبول کرد</string>
|
||||
<string name="this_setting_is_for_your_current_profile">این تنظیمات برای پروفایل فعلی شماست</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">درخواستهای تماس از گروهها</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">درخواست اتصال از گروه %1$s</string>
|
||||
<string name="open_to_use_bot">برای استفاده از ربات باز کنید</string>
|
||||
|
||||
@@ -1764,13 +1764,11 @@
|
||||
<string name="network_smp_proxy_mode_always">Toujours</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Routage privé</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Autoriser la rétrogradation</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mode de routage des messages</string>
|
||||
<string name="network_smp_proxy_mode_never">Jamais</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Serveurs inconnus</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Non</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Lorsque l\'IP est masquée</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Oui</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Rabattement du routage des messages</string>
|
||||
<string name="private_routing_show_message_status">Afficher le statut du message</string>
|
||||
<string name="protect_ip_address">Protection de l\'adresse IP</string>
|
||||
<string name="settings_section_title_files">Fichiers</string>
|
||||
@@ -1803,7 +1801,6 @@
|
||||
<string name="v5_8_private_routing_descr">Protégez votre adresse IP des relais de messagerie choisis par vos contacts.
|
||||
\nActivez-le dans les paramètres *Réseau et serveurs*.</string>
|
||||
<string name="chat_theme_reset_to_user_theme">Réinitialiser au thème de l\'utilisateur</string>
|
||||
<string name="chat_list_always_visible">Afficher la liste des conversations dans une nouvelle fenêtre</string>
|
||||
<string name="color_wallpaper_tint">Teinte du fond d\'écran</string>
|
||||
<string name="color_wallpaper_background">Fond d\'écran</string>
|
||||
<string name="message_queue_info_server_info">info sur la file du serveur : %1$s
|
||||
@@ -1979,9 +1976,7 @@
|
||||
<string name="privacy_media_blur_radius_off">Off</string>
|
||||
<string name="privacy_media_blur_radius_soft">Léger</string>
|
||||
<string name="toolbar_settings">Paramètres</string>
|
||||
<string name="info_view_connect_button">se connecter</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_open_button">ouvrir</string>
|
||||
<string name="confirm_delete_contact_question">Confirmer la suppression du contact ?</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Le contact sera supprimé — cette action est irréversible !</string>
|
||||
<string name="delete_without_notification">Supprimer sans notification</string>
|
||||
@@ -2171,7 +2166,6 @@
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Appareils Xiaomi</b> : veuillez activer le démarrage automatique dans les paramètres du système pour que les notifications fonctionnent.]]></string>
|
||||
<string name="delete_chat_for_all_members_cannot_undo_warning">La conversation sera supprimée pour tous les membres — cette action est irréversible !</string>
|
||||
<string name="delete_chat_for_self_cannot_undo_warning">La conversation sera supprimée pour vous — cette action est irréversible !</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Les conditions seront acceptées pour les opérateurs activés après 30 jours.</string>
|
||||
<string name="sync_connection_desc">La connexion nécessite une renégociation du chiffrement.</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Lien utilisable une seule fois <i>avec un seul contact</i> - partagez en personne ou via n\'importe quelle messagerie.]]></string>
|
||||
<string name="address_or_1_time_link">Adresse ou lien unique ?</string>
|
||||
@@ -2188,7 +2182,6 @@
|
||||
<string name="no_message_servers_configured_for_receiving">Pas de serveurs pour recevoir des messages.</string>
|
||||
<string name="message_deleted_or_not_received_error_title">Pas de message</string>
|
||||
<string name="for_social_media">Pour les réseaux sociaux</string>
|
||||
<string name="onboarding_network_operators_review_later">Vérifier plus tard</string>
|
||||
<string name="network_preset_servers_title">Serveurs prédéfinis</string>
|
||||
<string name="report_compose_reason_header_other">Signaler autre : seuls les modérateurs du groupe le verront.</string>
|
||||
<string name="debug_logs">Activer les journaux (logs)</string>
|
||||
@@ -2216,7 +2209,6 @@
|
||||
<string name="error_server_protocol_changed">Le protocole du serveur a été modifié.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Activez Flux dans les paramètres Réseau et serveurs pour une meilleure confidentialité des métadonnées.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Ce message a été supprimé ou n\'a pas encore été reçu.</string>
|
||||
<string name="address_creation_instruction">Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement.</string>
|
||||
<string name="share_address_publicly">Partager publiquement votre adresse</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux.</string>
|
||||
<string name="only_chat_owners_can_change_prefs">Seuls les propriétaires peuvent modifier les préférences.</string>
|
||||
@@ -2245,7 +2237,6 @@
|
||||
<string name="no_media_servers_configured_for_sending">Pas de serveurs pour envoyer des fichiers.</string>
|
||||
<string name="maximum_message_size_reached_text">Veuillez réduire la taille du message et envoyer le à nouveau.</string>
|
||||
<string name="maximum_message_size_reached_non_text">Veuillez réduire la taille du message ou supprimer le média et renvoyer le message.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">pour une meilleure protection des métadonnées.</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- Premier message non lu à l\'ouverture.\n- Sauter aux messages cités.</string>
|
||||
<string name="v6_2_business_chats_descr">Respect de la vie privée de vos clients.</string>
|
||||
<string name="v6_2_network_decentralization_descr">Le deuxième opérateur prédéfini de l\'application !</string>
|
||||
@@ -2260,7 +2251,6 @@
|
||||
<string name="connection_error_quota_desc">La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne.</string>
|
||||
<string name="server_added_to_operator__name">Serveur ajouté à l\'opérateur %s.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">L\'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Sélectionnez les opérateurs de réseau à utiliser.</string>
|
||||
<string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat et Flux ont conclu un accord pour intégrer à l’application des serveurs opérés par Flux.</string>
|
||||
<string name="chat_list_notes">Notes</string>
|
||||
<string name="or_to_share_privately">Ou à partager en privé</string>
|
||||
@@ -2272,17 +2262,14 @@
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Pour vous protéger contre le remplacement de votre lien, vous pouvez comparer les codes de sécurité des contacts.</string>
|
||||
<string name="how_it_helps_privacy">Comment il contribue à la protection de la vie privée</string>
|
||||
<string name="report_reason_alert_title">Motif du signalement ?</string>
|
||||
<string name="report_archive_alert_desc">Le rapport sera archivé pour vous.</string>
|
||||
<string name="report_reason_illegal">Contenu inapproprié</string>
|
||||
<string name="smp_servers_new_server">Nouveau serveur</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Vous pouvez configurer les serveurs via les paramètres.</string>
|
||||
<string name="operator_use_for_sending">Pour envoyer</string>
|
||||
<string name="report_reason_profile">Profil inapproprié</string>
|
||||
<string name="operator_website">Site web</string>
|
||||
<string name="list_menu">Liste</string>
|
||||
<string name="group_reports_member_reports">Signalement des membres</string>
|
||||
<string name="change_order_chat_list_menu_action">Modifier l\'ordre</string>
|
||||
<string name="onboarding_network_operators_update">Mise à jour</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans cette discussion.</string>
|
||||
<string name="privacy_chat_list_open_links_no">Non</string>
|
||||
<string name="privacy_chat_list_open_links_yes">Oui</string>
|
||||
@@ -2310,9 +2297,7 @@
|
||||
<string name="v6_2_improved_chat_navigation">Navigation améliorée dans les discussions</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Lorsque plusieurs opérateurs sont activés, aucun d\'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui.</string>
|
||||
<string name="maximum_message_size_title">Le message est trop volumineux !</string>
|
||||
<string name="remote_hosts_section">Mobiles à distance</string>
|
||||
<string name="change_list">Modifier la liste</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs.</string>
|
||||
<string name="appearance_in_app_bars_alpha">Transparence</string>
|
||||
<string name="you_can_set_connection_name_to_remember">Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé.</string>
|
||||
<string name="enable_sending_member_reports">Autoriser le signalement des messages aux modérateurs.</string>
|
||||
@@ -2425,7 +2410,6 @@
|
||||
<string name="compose_view_send_request">Envoyer la demande</string>
|
||||
<string name="compose_view_send_request_without_message">Envoyer la demande sans message</string>
|
||||
<string name="v6_4_support_chat_descr">Envoyez vos commentaires privés aux groupes.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Envoyé à votre contact après la connexion.</string>
|
||||
<string name="error_relay_test_server_auth">Le serveur requiert une autorisation pour se connecter au relais, vérifiez le mot de passe.</string>
|
||||
<string name="server_warning">Avertissement du serveur</string>
|
||||
<string name="share_old_address_alert_button">Partager l\'ancienne adresse</string>
|
||||
@@ -2441,11 +2425,9 @@
|
||||
<string name="share_profile_via_link_alert_text">L\'adresse sera courte et votre profil sera partagé via l\'adresse.</string>
|
||||
<string name="share_group_profile_via_link_alert_text">Le lien sera court, et le profil de groupe sera partagé via le lien.</string>
|
||||
<string name="the_sender_will_not_be_notified">L\'expéditeur n\'en sera PAS informé.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Ce réglage est pour votre profil actuel</string>
|
||||
<string name="channel_members_section_owners">Propriétaires & contributeurs</string>
|
||||
<string name="group_member_role_member_channel">contributeur</string>
|
||||
<string name="profile_description__field">Description</string>
|
||||
<string name="add_button">Ajouter</string>
|
||||
<string name="add_description">Ajouter une description</string>
|
||||
<string name="onboarding_your_network">Votre réseau</string>
|
||||
<string name="channel_member_you">vous</string>
|
||||
@@ -2456,7 +2438,6 @@
|
||||
<string name="connect_plan_this_is_your_link_for_channel">Votre canal</string>
|
||||
<string name="channel_link">Lien de canal</string>
|
||||
<string name="chat_link_channel">Lien de canal</string>
|
||||
<string name="button_channel_members">Membres du canal</string>
|
||||
<string name="channel_display_name_field">Nom du canal</string>
|
||||
<string name="channel_preferences">Préférences du canal</string>
|
||||
<string name="snd_channel_event_channel_profile_updated">profil du canal mis à jour</string>
|
||||
@@ -2504,7 +2485,6 @@
|
||||
<string name="content_filter_videos">Vidéos</string>
|
||||
<string name="content_filter_voice_messages">Messages vocaux</string>
|
||||
<string name="content_filter_links">Liens</string>
|
||||
<string name="content_filter_menu_item">Filtre</string>
|
||||
<string name="from_history">Depuis l\'historique</string>
|
||||
<string name="talk_to_someone">Parler à quelqu\'un</string>
|
||||
<string name="let_someone_connect_to_you">Permettez à quelqu\'un de se connecter à vous</string>
|
||||
@@ -2561,11 +2541,6 @@
|
||||
<string name="private_routing_timeout">Délai d\'attente dépassé pour le routage privé</string>
|
||||
<string name="smp_proxy_error_unknown_ca">L’empreinte de l’adresse du serveur de transfert ne correspond pas au certificat : %1$s.</string>
|
||||
<string name="proxy_destination_error_unknown_ca">L’empreinte de l’adresse du serveur de destination ne correspond pas au certificat : %1$s.</string>
|
||||
<string name="unsupported_channel_name">Nom de canal non pris en charge</string>
|
||||
<string name="unsupported_contact_name">Nom de contact non pris en charge</string>
|
||||
<string name="channel_name_requires_newer_app_version">La connexion via un nom de canal nécessite une version plus récente de l\'application.</string>
|
||||
<string name="contact_name_requires_newer_app_version">La connexion via un nom de contact nécessite une version plus récente de l\'application.</string>
|
||||
<string name="please_upgrade_the_app">Veuillez mettre à jour l\'application.</string>
|
||||
<string name="simplex_name_error">Erreur de nom SimpleX</string>
|
||||
<string name="simplex_name_no_servers_desc">Aucun de vos serveurs n\'est configuré pour résoudre les noms SimpleX. Configurez des serveurs ou utilisez un lien de connexion.</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">Le serveur %1$s ne prend pas en charge la résolution de nom. Configurez les serveurs ou utilisez un lien de connexion.</string>
|
||||
@@ -2605,11 +2580,9 @@
|
||||
<string name="signature_missing_alert_desc">Le canal exigeait que ce message soit signé, mais la signature est manquante.</string>
|
||||
<string name="accept_contact_request">Accepter la demande de contact</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Le membre a été supprimé : impossible d\'accepter la demande.</string>
|
||||
<string name="unread_mentions">Mentions non lues</string>
|
||||
<string name="verify_simplex_name_action">Vérifier le nom</string>
|
||||
<string name="verify_simplex_names">Vérifier les noms SimpleX</string>
|
||||
<string name="simplex_name_not_verified">Nom SimpleX non vérifié</string>
|
||||
<string name="simplex_name">Nom SimpleX</string>
|
||||
<string name="your_simplex_name">Votre nom SimpleX</string>
|
||||
<string name="set_simplex_name">Définir un nom SimpleX</string>
|
||||
<string name="error_saving_simplex_name">Erreur lors de l\'enregistrement du nom</string>
|
||||
@@ -2676,7 +2649,6 @@
|
||||
<string name="alert_text_msg_reception_error">L\'application a supprimé ce message après %1$d tentatives de réception.</string>
|
||||
<string name="sanitize_links_toggle">Supprimer le suivi des liens</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Demandes de contact dans les groupes</string>
|
||||
<string name="settings_section_title_about">À propos</string>
|
||||
<string name="settings_section_title_contact">Contact</string>
|
||||
<string name="settings_section_title_support_project">Soutenir le projet</string>
|
||||
<string name="help_and_support">Aide et support</string>
|
||||
@@ -2906,9 +2878,7 @@
|
||||
<string name="channel_will_start_with_relays">Le canal commencera à fonctionner avec %1$d relais sur %2$d. Continuer ?</string>
|
||||
<string name="relay_address_alert_message">Il s’agit d’une adresse de relais de messagerie, elle ne peut pas être utilisée pour se connecter.</string>
|
||||
<string name="connect_plan_open_channel">Ouvrir le canal</string>
|
||||
<string name="connect_plan_open_new_channel">Ouvrir un nouveau canal</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Voici votre lien pour le canal <b>%1$s</b> !]]></string>
|
||||
<string name="error_opening_channel">Erreur lors de l\'ouverture du canal</string>
|
||||
<string name="unblock_subscriber_for_all_question">Débloquer l\'abonné pour tout le monde ?</string>
|
||||
<string name="link_previews_alert_title">Activer les aperçus de liens ?</string>
|
||||
<string name="link_previews_alert_desc_socks">L’aperçu du lien sera demandé via le proxy SOCKS. La résolution DNS peut néanmoins toujours être effectuée localement via votre résolveur DNS.</string>
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
<string name="delete_messages__question">Odstraniti %d poruke?</string>
|
||||
<string name="delete_verb">Odstraniti</string>
|
||||
<string name="toolbar_settings">Podešavanje</string>
|
||||
<string name="onboarding_network_operators_update">Ažuriraj</string>
|
||||
<string name="delete_files_and_media_all">Odstraniti sve datoteke</string>
|
||||
<string name="delete_chat_profile_question">Odstraniti profil razgovora?</string>
|
||||
<string name="messages_section_title">Poruke</string>
|
||||
@@ -196,7 +195,6 @@
|
||||
<string name="button_add_friends">Dodaj prijatelja</string>
|
||||
<string name="button_add_team_members">Dodaj člana tima</string>
|
||||
<string name="archive_verb">Arhiva</string>
|
||||
<string name="info_view_connect_button">poveži</string>
|
||||
<string name="use_camera_button">Kamera</string>
|
||||
<string name="icon_descr_more_button">Više</string>
|
||||
<string name="auth_unlock">Otključaj</string>
|
||||
@@ -502,7 +500,6 @@
|
||||
<string name="contact_list_header_title">Vaši kontakti</string>
|
||||
<string name="network_use_onion_hosts_no">Ne</string>
|
||||
<string name="start_chat_question">Započeti razgovor?</string>
|
||||
<string name="info_view_open_button">otvoriti</string>
|
||||
<string name="onboarding_network_operators_continue">Nastaviti</string>
|
||||
<string name="rcv_group_event_open_chat">Otvoriti</string>
|
||||
<string name="color_background">Pozadina</string>
|
||||
@@ -865,7 +862,6 @@
|
||||
<string name="group_member_role_author">autor</string>
|
||||
<string name="export_theme">Izvesti temu</string>
|
||||
<string name="v4_4_live_messages">Žive poruke</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="custom_time_unit_days">dani</string>
|
||||
<string name="compose_forward_messages_n">Prosleđivanje %1$s poruka</string>
|
||||
<string name="network_use_onion_hosts_prefer">Kada je dostupno</string>
|
||||
@@ -880,7 +876,6 @@
|
||||
<string name="paste_the_link_you_received">Nalepiti link koji ste primili</string>
|
||||
<string name="rate_the_app">Oceniti aplikaciju</string>
|
||||
<string name="no_unread_chats">Nema nepročitanih razgovora</string>
|
||||
<string name="report_archive_alert_desc">Izveštaj će biti arhiviran za Vas.</string>
|
||||
<string name="keep_unused_invitation_question">Sačuvati neiskorišćenu pozivnicu?</string>
|
||||
<string name="voice_message_send_text">Glasovna poruka…</string>
|
||||
<string name="connect_use_current_profile">Koristiti trenutni profil</string>
|
||||
@@ -1110,7 +1105,6 @@
|
||||
<string name="la_lock_mode_passcode">Unos pin koda</string>
|
||||
<string name="message_forwarded_title">Prosleđena poruka</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Ništa za prosleđivanje</string>
|
||||
<string name="onboarding_network_operators_review_later">Pregledati kasnije</string>
|
||||
<string name="status_contact_has_e2e_encryption">kontakt ima e2e šifrovanje</string>
|
||||
<string name="lock_after">Zaključati nakon</string>
|
||||
<string name="notification_preview_somebody">Skriven kontakt:</string>
|
||||
|
||||
@@ -1749,11 +1749,9 @@
|
||||
<string name="network_smp_proxy_mode_private_routing">Privát útválasztás</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Privát útválasztás használata az ismeretlen kiszolgálókhoz.</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Privát útválasztás használata minden esetben.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Üzenet-útválasztási mód</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Közvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string>
|
||||
<string name="private_routing_explanation">Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Üzenet-útválasztási tartalék</string>
|
||||
<string name="settings_section_title_private_message_routing">Privát üzenet-útválasztás</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Privát útválasztás használata az ismeretlen kiszolgálókkal, ha az IP-cím nem védett.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string>
|
||||
@@ -1778,7 +1776,6 @@
|
||||
<string name="settings_section_title_chat_theme">Csevegés témája</string>
|
||||
<string name="wallpaper_scale_fill">Kitöltés</string>
|
||||
<string name="settings_section_title_user_theme">Profiltéma</string>
|
||||
<string name="chat_list_always_visible">Csevegési lista megjelenítése új ablakban</string>
|
||||
<string name="color_mode_light">Világos</string>
|
||||
<string name="chat_theme_apply_to_light_mode">Világos mód</string>
|
||||
<string name="color_received_quote">Fogadott válaszüzenet-buborék színe</string>
|
||||
@@ -1952,7 +1949,6 @@
|
||||
<string name="info_view_call_button">hívás</string>
|
||||
<string name="delete_contact_cannot_undo_warning">A partner törölve lesz – ez a művelet nem vonható vissza!</string>
|
||||
<string name="only_delete_conversation">Csak a beszélgetés törlése</string>
|
||||
<string name="info_view_open_button">megnyitás</string>
|
||||
<string name="conversation_deleted">Beszélgetés törölve!</string>
|
||||
<string name="contact_deleted">Partner törölve!</string>
|
||||
<string name="deleted_chats">Archivált partnerek</string>
|
||||
@@ -1969,7 +1965,6 @@
|
||||
<string name="info_view_message_button">üzenet</string>
|
||||
<string name="keep_conversation">Beszélgetés megtartása</string>
|
||||
<string name="confirm_delete_contact_question">Biztosan törli a partnert?</string>
|
||||
<string name="info_view_connect_button">kapcsolódás</string>
|
||||
<string name="one_hand_ui">Könnyen elérhető eszköztárak</string>
|
||||
<string name="delete_without_notification">Törlés értesítés nélkül</string>
|
||||
<string name="toolbar_settings">Beállítások</string>
|
||||
@@ -2098,7 +2093,6 @@
|
||||
<string name="connection_error_quota_desc">A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van.</string>
|
||||
<string name="message_deleted_or_not_received_error_title">Nincs üzenet</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Ez az üzenet törölve lett vagy még nem érkezett meg.</string>
|
||||
<string name="address_creation_instruction">Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz.</string>
|
||||
<string name="share_address_publicly">Cím nyilvános megosztása</string>
|
||||
<string name="share_simplex_address_on_social_media">SimpleX-cím megosztása a közösségi médiában.</string>
|
||||
<string name="share_1_time_link_with_a_friend">Egyszer használható meghívó megosztása egy baráttal</string>
|
||||
@@ -2115,12 +2109,7 @@
|
||||
<string name="onboarding_network_operators">Hálózatüzemeltetők</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Például, ha a partnere egy SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux kiszolgálón keresztül fogja azokat kézbesíteni.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Jelölje ki a használni kívánt hálózatüzemeltetőket.</string>
|
||||
<string name="onboarding_network_operators_review_later">Felülvizsgálat később</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string>
|
||||
<string name="onboarding_network_operators_update">Frissítés</string>
|
||||
<string name="onboarding_network_operators_continue">Folytatás</string>
|
||||
<string name="operator_review_conditions">Feltételek felülvizsgálata</string>
|
||||
<string name="operator_conditions_accepted">Elfogadott feltételek</string>
|
||||
@@ -2130,7 +2119,6 @@
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[A feltételek el lesznek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string>
|
||||
<string name="operator_servers_title">%s kiszolgáló</string>
|
||||
<string name="operator_info_title">Hálózatüzemeltető</string>
|
||||
<string name="operator_website">Weboldal</string>
|
||||
<string name="operator_conditions_accepted_on">Feltételek elfogadásának ideje: %s.</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">A feltételek el lesznek fogadva a következő időpontban: %s.</string>
|
||||
<string name="operator_use_operator_toggle_description">Kiszolgálók használata</string>
|
||||
@@ -2166,13 +2154,11 @@
|
||||
<string name="v6_2_network_decentralization_descr">A második előre beállított üzemeltető az alkalmazásban!</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">A Flux kiszolgálókat engedélyezheti a beállításokban, a „Hálózat és kiszolgálók” menüben, a metaadatok jobb védelme érdekében.</string>
|
||||
<string name="appearance_app_toolbars">Eszköztárak</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">a metaadatok jobb védelme érdekében.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Továbbfejlesztett csevegési navigáció</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- Csevegés megnyitása az első olvasatlan üzenetnél.\n- Ugrás az idézett üzenetekre.</string>
|
||||
<string name="view_updated_conditions">Frissített feltételek megtekintése</string>
|
||||
<string name="xftp_servers_per_user">A jelenlegi csevegési profiljához tartozó új fájlok kiszolgálói</string>
|
||||
<string name="chat_archive">Vagy archívumfájl importálása</string>
|
||||
<string name="remote_hosts_section">Távoli hordozható eszközök</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi eszközök</b>: engedélyezze az automatikus indítást a rendszerbeállításokban, hogy az értesítések működjenek.]]></string>
|
||||
<string name="maximum_message_size_reached_forwarding">Másolhatja és csökkentheti az üzenet méretét a küldéshez.</string>
|
||||
<string name="add_your_team_members_to_conversations">Adja hozzá a munkatársait a beszélgetésekhez.</string>
|
||||
@@ -2253,7 +2239,6 @@
|
||||
<string name="change_order_chat_list_menu_action">Elrendezés módosítása</string>
|
||||
<string name="error_creating_report">Hiba történt a jelentés létrehozásakor</string>
|
||||
<string name="prefs_error_saving_settings">Hiba történt a beállítások mentésekor</string>
|
||||
<string name="report_archive_alert_desc">A jelentés archiválva lesz az Ön számára.</string>
|
||||
<string name="report_compose_reason_header_illegal">Tartalom jelentése: csak a csoport moderátorai látják.</string>
|
||||
<string name="archive_verb">Archívum</string>
|
||||
<string name="report_archive_alert_title">Archiválja a jelentést?</string>
|
||||
@@ -2302,7 +2287,6 @@
|
||||
<string name="network_smp_web_port_footer">A következő TCP-port használata, amikor nincs port megadva: %1$s.</string>
|
||||
<string name="network_smp_web_port_section_title">TCP-port az üzenetváltáshoz</string>
|
||||
<string name="network_smp_web_port_toggle">Webport használata</string>
|
||||
<string name="unread_mentions">Olvasatlan említések</string>
|
||||
<string name="mute_all_chat">Összes némítása</string>
|
||||
<string name="max_group_mentions_per_message_reached">Legfeljebb %1$s tagot említhet meg egy üzenetben!</string>
|
||||
<string name="enable_sending_member_reports">Az üzenetek jelentése a moderátorok felé engedélyezve van.</string>
|
||||
@@ -2431,7 +2415,6 @@
|
||||
<string name="reject_contact_request">Partneri kapcsolatkérés elutasítása</string>
|
||||
<string name="the_sender_will_not_be_notified">A kérés küldője NEM lesz értesítve.</string>
|
||||
<string name="context_user_picker_your_profile">Saját profil</string>
|
||||
<string name="sent_to_your_contact_after_connection">Elküldés a partnernek a kapcsolódást követően.</string>
|
||||
<string name="address_welcome_message">Üdvözlőüzenet</string>
|
||||
<string name="share_profile_via_link">Frissíti a címet?</string>
|
||||
<string name="share_profile_via_link_alert_text">A cím rövid lesz és a profil meg lesz osztva a címen keresztül.</string>
|
||||
@@ -2486,7 +2469,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">Partneri kapcsolatkérések a csoportok tagjaitól</string>
|
||||
<string name="member_is_deleted_cant_accept_request">A tag törölve lett – nem lehet elfogadni a kérést</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">a(z) %1$s nevű csoportból partneri kapcsolatot kért</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Ez a beállítás a jelenlegi profiljára vonatkozik</string>
|
||||
<string name="open_to_use_bot">Megnyitás a bot használatához</string>
|
||||
<string name="chat_banner_connect_to_use_bot">Koppintson a „Kapcsolódás” gombra a bot használatához</string>
|
||||
<string name="chat_banner_bot">Bot</string>
|
||||
@@ -2516,7 +2498,6 @@
|
||||
<string name="remove_member_delete_messages_confirmation">Eltávolítás és az üzeneteinek törlése</string>
|
||||
<string name="content_filter_all_messages">Összes üzenet</string>
|
||||
<string name="content_filter_files">Fájlok</string>
|
||||
<string name="content_filter_menu_item">Szűrő</string>
|
||||
<string name="content_filter_images">Képek</string>
|
||||
<string name="content_filter_links">Hivatkozások</string>
|
||||
<string name="placeholder_search_files">Fájlok keresése</string>
|
||||
@@ -2535,7 +2516,6 @@
|
||||
<string name="chat_banner_channel">Csatorna</string>
|
||||
<string name="info_row_channel">Csatorna</string>
|
||||
<string name="channel_link">Csatornahivatkozás</string>
|
||||
<string name="button_channel_members">Csatornatagok</string>
|
||||
<string name="channel_display_name_field">Csatorna neve</string>
|
||||
<string name="relay_test_step_connect">Kapcsolódás</string>
|
||||
<string name="relay_conn_status_connected">kapcsolódott</string>
|
||||
@@ -2550,7 +2530,6 @@
|
||||
<string name="relay_status_new">új</string>
|
||||
<string name="relay_status_invited">meghíva</string>
|
||||
<string name="connect_plan_open_channel">Csatorna megnyitása</string>
|
||||
<string name="connect_plan_open_new_channel">Új csatorna megnyitása</string>
|
||||
<string name="member_info_section_title_owner">Tulajdonos</string>
|
||||
<string name="channel_members_section_owners">Tulajdonosok és közreműködők</string>
|
||||
<string name="button_leave_channel">Csatorna elhagyása</string>
|
||||
@@ -2633,7 +2612,6 @@
|
||||
<string name="relay_address_alert_title">Átjátszó címe</string>
|
||||
<string name="relay_address_alert_message">Ez egy csevegési átjátszó címe, nem használható kapcsolódásra.</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Ez a saját hivatkozása a(z) <b>%1$s</b> nevű csatornához!]]></string>
|
||||
<string name="error_opening_channel">Hiba történt a csatorna megnyitásakor</string>
|
||||
<string name="unblock_subscriber_for_all_question">Az összes feliratkozó számára feloldja a feliratkozó letiltását?</string>
|
||||
<string name="test_relay_to_retrieve_name"><![CDATA[<b>Átjátszó tesztelése</b> a nevének lekéréséhez.]]></string>
|
||||
<string name="channel_full_name_field">Csatorna teljes neve:</string>
|
||||
@@ -2773,7 +2751,6 @@
|
||||
<string name="one_hand_ui_top_bar">Felső sáv</string>
|
||||
<string name="cancel_channel_alert_msg">Az új %1$s nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódott.\nHa visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja.</string>
|
||||
<string name="button_cancel_and_delete_channel">Visszavonás és a csatorna törlése</string>
|
||||
<string name="add_button">Hozzáadás</string>
|
||||
<string name="add_relay_button">Átjátszó hozzáadása</string>
|
||||
<string name="add_relays_title">Átjátszók hozzáadása</string>
|
||||
<string name="num_relays_selected">%d átjátszó kiválasztva</string>
|
||||
@@ -2805,14 +2782,8 @@
|
||||
<string name="member_info_relay_status_rejected_by_operator">az átjátszó üzemeltetője elutasította</string>
|
||||
<string name="another_instance_title">Az alkalmazás már fut</string>
|
||||
<string name="another_instance_not_responding">Lehet, hogy egy másik alkalmazáspéldány fut, vagy nem zárult be megfelelően. Így is elindítja?</string>
|
||||
<string name="unsupported_channel_name">Nem támogatott csatornanév</string>
|
||||
<string name="unsupported_contact_name">Nem támogatott partnernév</string>
|
||||
<string name="channel_name_requires_newer_app_version">A csatorna nevén keresztüli kapcsolódáshoz újabb alkalmazásverzió szükséges.</string>
|
||||
<string name="contact_name_requires_newer_app_version">A partner nevén keresztüli kapcsolódáshoz újabb alkalmazásverzió szükséges.</string>
|
||||
<string name="please_upgrade_the_app">Frissítse az alkalmazást.</string>
|
||||
<string name="app_update_required">Alkalmazásfrissítés szükséges</string>
|
||||
<string name="group_link_requires_newer_version">Ehhez a csoporthoz az alkalmazás újabb verziója szükséges. A csatlakozáshoz frissítse az alkalmazást.</string>
|
||||
<string name="settings_section_title_about">Névjegy</string>
|
||||
<string name="settings_section_title_contact">Kapcsolat</string>
|
||||
<string name="settings_section_title_support_project">A projekt támogatása</string>
|
||||
<string name="chat_data">Csevegési adatok</string>
|
||||
@@ -2862,7 +2833,6 @@
|
||||
<string name="verify_simplex_name_action">Név ellenőrzése</string>
|
||||
<string name="verify_simplex_names">SimpleX-nevek ellenőrzése</string>
|
||||
<string name="simplex_name_not_verified">Nincs ellenőrizve a SimpleX-név</string>
|
||||
<string name="simplex_name">SimpleX-név</string>
|
||||
<string name="your_simplex_name">Saját SimpleX-név</string>
|
||||
<string name="set_simplex_name">SimpleX-név beállítása</string>
|
||||
<string name="error_saving_simplex_name">Hiba történt a név mentésekor</string>
|
||||
|
||||
@@ -545,7 +545,6 @@
|
||||
<string name="videos_limit_title">Terlalu banyak video!</string>
|
||||
<string name="voice_message">Pesan suara</string>
|
||||
<string name="info_view_message_button">pesan</string>
|
||||
<string name="info_view_open_button">buka</string>
|
||||
<string name="la_mode_system">Sistem</string>
|
||||
<string name="profile_update_event_set_new_address">setel alamat kontak baru</string>
|
||||
<string name="dark_theme">Tema gelap</string>
|
||||
@@ -969,8 +968,6 @@
|
||||
<string name="core_version">Versi inti: v%s</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Ketika IP disembunyikan</string>
|
||||
<string name="theme_colors_section_title">Warna antarmuka</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Fallback perutean pesan</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mode routing pesan</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Routing pribadi</string>
|
||||
<string name="network_smp_proxy_mode_never_description">JANGAN gunakan routing pribadi.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Gunakan routing pribadi dengan server yang tak dikenal ketika alamat IP tidak dilindungi.</string>
|
||||
@@ -1330,7 +1327,6 @@
|
||||
<string name="create_group_button">Buat grup</string>
|
||||
<string name="group_full_name_field">Nama lengkap grup:</string>
|
||||
<string name="save_group_profile">Simpan profil grup</string>
|
||||
<string name="operator_website">Peramban</string>
|
||||
<string name="your_servers">Server Anda</string>
|
||||
<string name="error_saving_group_profile">Gagal simpan profil grup</string>
|
||||
<string name="operator_servers_title">%s server</string>
|
||||
@@ -1389,7 +1385,6 @@
|
||||
<string name="onboarding_choose_server_operators">Operator server</string>
|
||||
<string name="onboarding_network_operators">Operator jaringan</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Aplikasi ini melindungi privasi Anda dengan gunakan operator yang berbeda setiap percakapan.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Ketentuan akan diterima untuk operator yang diaktifkan setelah 30 hari.</string>
|
||||
<string name="settings_developer_tools">Alat pengembang</string>
|
||||
<string name="settings_section_title_chat_colors">Warna obrolan</string>
|
||||
<string name="rcv_group_event_member_created_contact">permintaan koneksi</string>
|
||||
@@ -1416,7 +1411,6 @@
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Ini adalah tautan untuk grup <b>%1$s</b>!]]></string>
|
||||
<string name="migrate_from_device_cancel_migration">Batal pindah</string>
|
||||
<string name="chunks_uploaded">Potongan diunggah</string>
|
||||
<string name="info_view_connect_button">hubungkan</string>
|
||||
<string name="conversation_deleted">Percakapan dihapus!</string>
|
||||
<string name="disappearing_message">Pesan sementara</string>
|
||||
<string name="clear_note_folder_question">Hapus catatan pribadi?</string>
|
||||
@@ -1487,7 +1481,6 @@
|
||||
<string name="connection_request_sent">Permintaan koneksi terkirim!</string>
|
||||
<string name="connection_security">Keamanan koneksi</string>
|
||||
<string name="correct_name_to">Nama telah benar untuk %s?</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Anda dapat konfigurasi operator di pengaturan Jaringan dan server.</string>
|
||||
<string name="database_error">Kesalahan basis data</string>
|
||||
<string name="passphrase_is_different">Frasa sandi basis data berbeda dengan yang disimpan di Keystore.</string>
|
||||
<string name="change_member_role_question">Ubah peran grup?</string>
|
||||
@@ -1589,14 +1582,11 @@
|
||||
<string name="onboarding_notifications_mode_off_desc_short">Tiada layanan latar belakang</string>
|
||||
<string name="onboarding_notifications_mode_service_desc_short">Aplikasi selalu berjalan di latar belakang</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc_short">Periksa pesan setiap 10 menit</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Pilih operator jaringan yang akan digunakan.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Misalnya, jika kontak Anda menerima pesan melalui server SimpleX Chat, aplikasi Anda akan mengirimkannya melalui server Flux.</string>
|
||||
<string name="how_it_helps_privacy">Bagaimana ini membantu privasi</string>
|
||||
<string name="onboarding_network_operators_continue">Lanjutkan</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Anda dapat konfigurasi server di pengaturan.</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[Lebih lengkap di <font color="#0088ff">repositori GitHub</font> kami.]]></string>
|
||||
<string name="onboarding_network_operators_review_later">Nilai nanti</string>
|
||||
<string name="onboarding_network_operators_update">Perbarui</string>
|
||||
<string name="migrate_to_device_downloading_details">Rincian tautan unduhan</string>
|
||||
<string name="network_session_mode_user_description"><![CDATA[Koneksi TCP terpisah (dan kredensial SOCKS) akan digunakan <b>untuk setiap profil obrolan yang Anda miliki di aplikasi</b>.]]></string>
|
||||
<string name="network_session_mode_entity_description"><![CDATA[Koneksi TCP terpisah (dan kredensial SOCKS) akan digunakan <b>untuk setiap kontak dan anggota grup</b>.\n<b>Harap diperhatikan</b>: jika Anda memiliki banyak koneksi, konsumsi baterai dan lalu lintas dapat jauh lebih tinggi dan beberapa koneksi mungkin gagal.]]></string>
|
||||
@@ -1696,7 +1686,6 @@
|
||||
<string name="conn_event_ratchet_sync_required">negosiasi ulang enkripsi diperlukan</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">negosiasi ulang enkripsi diizinkan untuk %s</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Aktifkan Flux di pengaturan Jaringan dan server untuk privasi metadata yang lebih baik.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">untuk privasi metadata lebih baik.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Navigasi obrolan ditingkatkan</string>
|
||||
<string name="error">Galat</string>
|
||||
<string name="migrate_to_device_finalize_migration">Selesaikan migrasi pada perangkat lain.</string>
|
||||
@@ -1907,7 +1896,6 @@
|
||||
<string name="you_can_share_your_address">Anda dapat bagikan alamat sebagai tautan atau kode QR - siapa pun dapat terhubung dengan Anda.</string>
|
||||
<string name="network_options_save">Simpan</string>
|
||||
<string name="make_profile_private">Jadikan profil pribadi!</string>
|
||||
<string name="remote_hosts_section">Ponsel jarak jauh</string>
|
||||
<string name="run_chat_section">Jalankan obrolan</string>
|
||||
<string name="store_passphrase_securely_without_recover">Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengakses obrolan jika hilang.</string>
|
||||
<string name="rcv_group_event_member_deleted">dihapus %1$s</string>
|
||||
@@ -2197,14 +2185,12 @@
|
||||
<string name="image_decoding_exception_desc">Gambar tidak dapat didekodekan. Silakan coba gambar lain atau hubungi pengembang.</string>
|
||||
<string name="snd_error_auth">Kunci salah atau koneksi tidak dikenal - kemungkinan besar koneksi ini dihapus.</string>
|
||||
<string name="add_contact_or_create_group">Mulai obrolan baru</string>
|
||||
<string name="address_creation_instruction">Ketuk Buat alamat SimpleX di menu untuk membuatnya nanti.</string>
|
||||
<string name="scan_qr_to_connect_to_contact">Untuk terhubung, kontak Anda dapat pindai kode QR atau gunakan tautan di aplikasi.</string>
|
||||
<string name="connect__your_profile_will_be_shared">Profil Anda %1$s akan dibagikan.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Untuk melindungi tautan Anda dari penggantian, Anda dapat membandingkan kode keamanan kontak.</string>
|
||||
<string name="app_check_for_updates_canceled">Pembaruan unduhan dibatalkan</string>
|
||||
<string name="your_current_profile">Profil Anda saat ini</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Anda menggunakan profil samaran untuk grup ini - untuk mencegah berbagi profil utama Anda, undang kontak tidak diizinkan</string>
|
||||
<string name="chat_list_always_visible">Lihat daftar obrolan di jendela baru</string>
|
||||
<string name="you_sent_group_invitation">Anda mengirim undangan grup</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Tautan ini bukan tautan koneksi yang valid!</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Anda akan berhenti menerima pesan dari grup ini. Riwayat obrolan akan disimpan.</string>
|
||||
@@ -2277,7 +2263,6 @@
|
||||
<string name="report_compose_reason_header_profile">Laporkan profil anggota: hanya moderator grup yang akan melihat.</string>
|
||||
<string name="report_archive_alert_title">Arsip laporan?</string>
|
||||
<string name="file_error_blocked">Berkas diblokir oleh operator server:\n\n%1$s.</string>
|
||||
<string name="report_archive_alert_desc">Laporan akan diarsipkan untuk Anda.</string>
|
||||
<string name="group_member_role_moderator">moderator</string>
|
||||
<string name="report_compose_reason_header_illegal">Laporkan konten: hanya moderator grup yang akan melihat.</string>
|
||||
<string name="change_list">Ubah daftar</string>
|
||||
@@ -2305,7 +2290,6 @@
|
||||
<string name="chat_item_ttl_year">1 tahun</string>
|
||||
<string name="chat_item_ttl_default">bawaan (%s)</string>
|
||||
<string name="mute_all_chat">Bisukan semua</string>
|
||||
<string name="unread_mentions">Sebutan belum terbaca</string>
|
||||
<string name="network_smp_web_port_section_title">Port TCP untuk pesan</string>
|
||||
<string name="network_smp_web_port_footer">Gunakan port TCP %1$s jika tidak ada port yang ditentukan.</string>
|
||||
<string name="network_smp_web_port_toggle">Gunakan port peramban</string>
|
||||
@@ -2474,7 +2458,6 @@
|
||||
<string name="compose_view_send_request">Kirim permintaan</string>
|
||||
<string name="compose_view_send_request_without_message">Kirim permintaan tanpa pesan</string>
|
||||
<string name="v6_4_support_chat_descr">Kirimkan masukan pribadi Anda ke grup.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Dikirim ke kontak Anda setelah terhubung.</string>
|
||||
<string name="v6_4_1_welcome_contacts_descr">Pasang bio profil dan pesan sambutan.</string>
|
||||
<string name="share_old_address_alert_button">Bagikan alamat lama</string>
|
||||
<string name="share_old_link_alert_button">Bagikan tautan lama</string>
|
||||
@@ -2496,7 +2479,6 @@
|
||||
<string name="share_profile_via_link_alert_text">Alamat akan singkat, dan profil Anda akan dibagikan melalui alamat tersebut.</string>
|
||||
<string name="share_group_profile_via_link_alert_text">Tautan akan singkat, dan profil grup akan dibagikan melalui tautan tersebut.</string>
|
||||
<string name="the_sender_will_not_be_notified">Pengirim TIDAK akan diberi tahu.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Pengaturan ini untuk profil Anda saat ini</string>
|
||||
<string name="time_to_disappear_is_set_only_for_new_contacts">Waktu menghilang hanya diatur untuk kontak baru.</string>
|
||||
<string name="cant_send_commands_alert_text">Untuk mengirim perintah, Anda harus terhubung.</string>
|
||||
<string name="context_user_picker_cant_change_profile_alert_message">Untuk menggunakan profil lain setelah mencoba hubungkan, hapus chat dan gunakan tautan lagi.</string>
|
||||
@@ -2535,11 +2517,9 @@
|
||||
<string name="channel_subscriber_count_singular">%1$d pelanggan</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d pelanggan</string>
|
||||
<string name="badge_supported_simplex">%1$s mendukung SimpleX Chat. Lencana berakhir pada %2$s.</string>
|
||||
<string name="settings_section_title_about">Tentang</string>
|
||||
<string name="relay_status_accepted">diterima</string>
|
||||
<string name="relay_status_acknowledged_roster">daftar yang di akui</string>
|
||||
<string name="relay_status_active">aktif</string>
|
||||
<string name="add_button">Tambahkan</string>
|
||||
<string name="add_relay_button">Tambah relay</string>
|
||||
<string name="add_relays_title">Tambah relay</string>
|
||||
<string name="relay_bar_owner_no_delivery">Tambahkan relay untuk memulihkan pengiriman pesan.</string>
|
||||
@@ -2572,7 +2552,6 @@
|
||||
<string name="cant_broadcast_message">Gagal menyiarkan</string>
|
||||
<string name="channel_role_label">Saluran</string>
|
||||
<string name="chat_link_channel">Tautan saluran</string>
|
||||
<string name="button_channel_members">Anggota saluran</string>
|
||||
<string name="channel_display_name_field">Nama saluran</string>
|
||||
<string name="channel_preferences">Preferensi saluran</string>
|
||||
<string name="channel_profile_is_stored_on_subscribers_devices">Profil saluran disimpan di perangkat pelanggan dan di relai obrolan.</string>
|
||||
@@ -2602,8 +2581,6 @@
|
||||
<string name="relay_test_step_connect">Hubungkan</string>
|
||||
<string name="relay_conn_status_connected">terhubung</string>
|
||||
<string name="relay_conn_status_connecting">menghubungkan</string>
|
||||
<string name="channel_name_requires_newer_app_version">Menghubungkan melalui nama saluran memerlukan versi aplikasi yang lebih baru.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Menghubungkan melalui nama kontak memerlukan versi aplikasi yang lebih baru.</string>
|
||||
<string name="info_row_connection_failed">Koneksi gagal</string>
|
||||
<string name="connect_via_link_or_qr_code">Hubungkan melalui tautan atau kode QR</string>
|
||||
<string name="settings_section_title_contact">Kontak</string>
|
||||
@@ -2648,7 +2625,6 @@
|
||||
<string name="error_adding_relays">Galat saat menambahkan relai</string>
|
||||
<string name="error_creating_channel">Galat saat membuat saluran</string>
|
||||
<string name="error_deleting_message">Gagal menghapus pesan</string>
|
||||
<string name="error_opening_channel">Galat saat membuka saluran</string>
|
||||
<string name="rcv_msg_error_parse">galat: %s</string>
|
||||
<string name="error_saving_channel_profile">Galat saat menyimpan profil saluran</string>
|
||||
<string name="error_sharing_channel">Galat saat membagikan saluran</string>
|
||||
@@ -2656,7 +2632,6 @@
|
||||
<string name="relay_conn_status_failed">gagal</string>
|
||||
<string name="relay_status_failed">gagal</string>
|
||||
<string name="content_filter_files">Berkas</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="for_anyone_to_reach_you">Agar siapa pun dapat menghubungi Anda</string>
|
||||
<string name="from_history">Dari riwayat</string>
|
||||
<string name="chat_link_from_owner">(dari pemilik)</string>
|
||||
@@ -2705,9 +2680,6 @@
|
||||
<string name="relay_bar_no_relays">Tidak ada relai</string>
|
||||
<string name="no_relays_selected">Tidak ada relay yang dipilih</string>
|
||||
<string name="voice_recording_not_supported">Perekaman suara tidak didukung di platform Anda.</string>
|
||||
<string name="unsupported_channel_name">Nama saluran tidak didukung</string>
|
||||
<string name="unsupported_contact_name">Nama kontak tidak didukung</string>
|
||||
<string name="please_upgrade_the_app">Silakan perbarui aplikasinya.</string>
|
||||
<string name="group_link_requires_newer_version">Grup ini membutuhkan versi aplikasi yang lebih baru. Silakan perbarui aplikasi untuk bergabung.</string>
|
||||
<string name="placeholder_search_images">Cari gambar</string>
|
||||
<string name="placeholder_search_videos">Cari video</string>
|
||||
|
||||
@@ -1772,12 +1772,10 @@
|
||||
<string name="network_smp_proxy_mode_never">Mai</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Server sconosciuti</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Usa l\'instradamento privato con server sconosciuti.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Modalità instradamento messaggio</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Usa l\'instradamento privato con server sconosciuti quando l\'indirizzo IP non è protetto.</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Sì</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l\'instradamento privato.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Quando l\'IP è nascosto</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Ripiego instradamento messaggio</string>
|
||||
<string name="private_routing_show_message_status">Mostra stato del messaggio</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Consenti downgrade</string>
|
||||
<string name="network_smp_proxy_mode_always">Sempre</string>
|
||||
@@ -1819,7 +1817,6 @@
|
||||
<string name="wallpaper_scale_fit">Adatta</string>
|
||||
<string name="color_sent_quote">Risposta inviata</string>
|
||||
<string name="chat_theme_set_default_theme">Imposta tema predefinito</string>
|
||||
<string name="chat_list_always_visible">Mostra la lista di chat in una nuova finestra</string>
|
||||
<string name="color_mode_system">Sistema</string>
|
||||
<string name="color_wallpaper_tint">Tinta dello sfondo</string>
|
||||
<string name="wallpaper_preview_hello_alice">Buon pomeriggio!</string>
|
||||
@@ -1989,7 +1986,6 @@
|
||||
<string name="privacy_media_blur_radius_strong">Forte</string>
|
||||
<string name="info_view_call_button">chiama</string>
|
||||
<string name="info_view_message_button">messaggio</string>
|
||||
<string name="info_view_open_button">apri</string>
|
||||
<string name="info_view_search_button">cerca</string>
|
||||
<string name="toolbar_settings">Impostazioni</string>
|
||||
<string name="delete_without_notification">Elimina senza avvisare</string>
|
||||
@@ -2016,7 +2012,6 @@
|
||||
<string name="cant_call_member_send_message_alert_text">Invia un messaggio per attivare le chiamate.</string>
|
||||
<string name="info_view_video_button">video</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Puoi ancora vedere la conversazione con %1$s nell\'elenco delle chat.</string>
|
||||
<string name="info_view_connect_button">connetti</string>
|
||||
<string name="contact_deleted">Contatto eliminato!</string>
|
||||
<string name="confirm_delete_contact_question">Confermare l\'eliminazione del contatto?</string>
|
||||
<string name="compose_message_placeholder">Chatta</string>
|
||||
@@ -2134,12 +2129,8 @@
|
||||
<string name="or_to_share_privately">O per condividere in modo privato</string>
|
||||
<string name="onboarding_network_operators">Operatori di rete</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">L\'app protegge la tua privacy usando diversi operatori per ogni conversazione.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Puoi configurare gli operatori nelle impostazioni di rete e server.</string>
|
||||
<string name="onboarding_choose_server_operators">Operatori del server</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Seleziona gli operatori di rete da usare.</string>
|
||||
<string name="onboarding_network_operators_continue">Continua</string>
|
||||
<string name="onboarding_network_operators_update">Aggiornamento</string>
|
||||
<string name="onboarding_network_operators_review_later">Leggi più tardi</string>
|
||||
<string name="network_preset_servers_title">Server preimpostati</string>
|
||||
<string name="operator_conditions_accepted">Condizioni accettate</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s.</string>
|
||||
@@ -2151,7 +2142,6 @@
|
||||
<string name="operator_info_title">Operatore di rete</string>
|
||||
<string name="operator_servers_title">Server di %s</string>
|
||||
<string name="use_servers_of_operator_x">Usa %s</string>
|
||||
<string name="operator_website">Sito web</string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Le condizioni verranno accettate per gli operatori: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_on">Condizioni accettate il: %s.</string>
|
||||
<string name="operator">Operatore</string>
|
||||
@@ -2189,18 +2179,15 @@
|
||||
<string name="address_or_1_time_link">Indirizzo o link una tantum?</string>
|
||||
<string name="address_settings">Impostazioni dell\'indirizzo</string>
|
||||
<string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Il link una tantum puo\' essere usato <i>con un solo contatto</i> - condividilo di persona o tramite un messenger.]]></string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni.</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Le condizioni verranno accettate il: %s.</string>
|
||||
<string name="error_accepting_operator_conditions">Errore di accettazione delle condizioni</string>
|
||||
<string name="failed_to_save_servers">Errore di salvataggio dei server</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">per una migliore privacy dei metadati.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Ad esempio, se il tuo contatto riceve i messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server di Flux.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Navigazione della chat migliorata</string>
|
||||
<string name="smp_servers_new_server">Nuovo server</string>
|
||||
<string name="operator_use_for_files">Usa per i file</string>
|
||||
<string name="simplex_address_or_1_time_link">Indirizzo SimpleX o link una tantum?</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Questo messaggio è stato eliminato o non ancora ricevuto.</string>
|
||||
<string name="address_creation_instruction">Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi.</string>
|
||||
<string name="connection_error_quota_desc">La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline.</string>
|
||||
<string name="operator_use_operator_toggle_description">Usa i server</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Puoi configurare i server nelle impostazioni.</string>
|
||||
@@ -2212,7 +2199,6 @@
|
||||
<string name="share_address_publicly">Condividi indirizzo pubblicamente</string>
|
||||
<string name="share_simplex_address_on_social_media">Condividi l\'indirizzo SimpleX sui social media.</string>
|
||||
<string name="chat_archive">O importa un file dell\'archivio</string>
|
||||
<string name="remote_hosts_section">Telefoni remoti</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questa chat.</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Dispositivi Xiaomi</b>: attiva l\'avvio automatico nelle impostazioni di sistema per fare funzionare le notifiche.]]></string>
|
||||
<string name="add_your_team_members_to_conversations">Aggiungi i membri del tuo team alle conversazioni.</string>
|
||||
@@ -2303,7 +2289,6 @@
|
||||
<string name="report_reason_spam">Spam</string>
|
||||
<string name="report_verb">Segnala</string>
|
||||
<string name="report_reason_alert_title">Motivo della segnalazione?</string>
|
||||
<string name="report_archive_alert_desc">La segnalazione verrà archiviata per te.</string>
|
||||
<string name="report_compose_reason_header_profile">Segnala profilo: solo i moderatori del gruppo lo vedranno.</string>
|
||||
<string name="report_compose_reason_header_spam">Segnala spam: solo i moderatori del gruppo lo vedranno.</string>
|
||||
<string name="report_compose_reason_header_community">Segnala violazione: solo i moderatori del gruppo lo vedranno.</string>
|
||||
@@ -2339,7 +2324,6 @@
|
||||
<string name="network_smp_web_port_section_title">Porta TCP per i messaggi</string>
|
||||
<string name="network_smp_web_port_toggle">Usa porta web</string>
|
||||
<string name="mute_all_chat">Silenzia tutto</string>
|
||||
<string name="unread_mentions">Menzioni non lette</string>
|
||||
<string name="max_group_mentions_per_message_reached">Puoi menzionare fino a %1$s membri per messaggio!</string>
|
||||
<string name="group_members_can_send_reports">I membri possono segnalare messaggi ai moderatori.</string>
|
||||
<string name="report_archive_alert_title_all">Archiviare tutte le segnalazioni?</string>
|
||||
@@ -2465,7 +2449,6 @@
|
||||
<string name="compose_view_send_contact_request_alert_question">Inviare una richiesta di contatto?</string>
|
||||
<string name="compose_view_send_request">Invia richiesta</string>
|
||||
<string name="compose_view_send_request_without_message">Invia richiesta senza messaggio</string>
|
||||
<string name="sent_to_your_contact_after_connection">Inviato al tuo contatto dopo la connessione.</string>
|
||||
<string name="share_group_profile_via_link">Aggiornare il link del gruppo?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Aggiorna</string>
|
||||
<string name="share_profile_via_link">Aggiornare l\'indirizzo?</string>
|
||||
@@ -2521,7 +2504,6 @@
|
||||
<string name="settings_section_title_contact_requests_from_groups">Richieste di contatto nei gruppi</string>
|
||||
<string name="member_is_deleted_cant_accept_request">Il membro è eliminato - impossibile accettare la richiesta</string>
|
||||
<string name="rcv_direct_event_group_inv_link_received">connessione richiesta dal gruppo %1$s</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Questa impostazione è per il tuo profilo attuale</string>
|
||||
<string name="allow_files_and_media_only_if">Consenti file e contenuti multimediali solo se il tuo contatto li consente.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Consenti ai tuoi contatti di inviare file e contenuti multimediali.</string>
|
||||
<string name="chat_banner_bot">Bot</string>
|
||||
@@ -2560,7 +2542,6 @@
|
||||
<string name="placeholder_search_voice_messages">Cerca messaggi vocali</string>
|
||||
<string name="content_filter_videos">Video</string>
|
||||
<string name="content_filter_voice_messages">Messaggi vocali</string>
|
||||
<string name="content_filter_menu_item">Filtro</string>
|
||||
<string name="info_row_connection_failed">Connessione fallita</string>
|
||||
<string name="member_info_member_failed">fallito</string>
|
||||
<string name="down_migration_warning_chat_relays">Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente.</string>
|
||||
@@ -2580,7 +2561,6 @@
|
||||
<string name="chat_banner_channel">Canale</string>
|
||||
<string name="info_row_channel">Canale</string>
|
||||
<string name="channel_link">Link del canale</string>
|
||||
<string name="button_channel_members">Membri del canale</string>
|
||||
<string name="channel_display_name_field">Nome del canale</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">Il canale verrà eliminato per tutti gli iscritti, non è reversibile!</string>
|
||||
<string name="delete_channel_for_self_cannot_undo_warning">Il canale verrà eliminato per te, non è reversibile!</string>
|
||||
@@ -2610,7 +2590,6 @@
|
||||
<string name="enter_relay_name">Inserisci il nome del relay…</string>
|
||||
<string name="error_adding_relay">Errore di aggiunta del relay</string>
|
||||
<string name="error_creating_channel">Errore di creazione del canale</string>
|
||||
<string name="error_opening_channel">Errore di apertura del canale</string>
|
||||
<string name="relay_conn_status_failed">fallito</string>
|
||||
<string name="relay_status_failed">fallito</string>
|
||||
<string name="relay_test_step_get_link">Ottieni link</string>
|
||||
@@ -2624,7 +2603,6 @@
|
||||
<string name="no_chat_relays_enabled">Nessun relay di chat attivato.</string>
|
||||
<string name="not_all_relays_connected">Non tutti i relay sono connessi</string>
|
||||
<string name="connect_plan_open_channel">Apri canale</string>
|
||||
<string name="connect_plan_open_new_channel">Apri il nuovo canale</string>
|
||||
<string name="member_info_section_title_owner">Proprietario</string>
|
||||
<string name="channel_members_section_owners">Proprietari e collaboratori</string>
|
||||
<string name="preset_relay_address">Indirizzo relay preimpostato</string>
|
||||
@@ -2807,7 +2785,6 @@
|
||||
<string name="link_previews_alert_desc_socks">L\'anteprima del link verrà richiesta via proxy SOCKS. La ricerca DNS può ancora accadere localmente tramite il tuo risolutore DNS.</string>
|
||||
<string name="one_hand_ui_top_bar">Barra superiore</string>
|
||||
<string name="cancel_channel_alert_msg">Il tuo nuovo canale %1$s è connesso a %2$d di %3$d relay.\nSe annulli, il canale verrà eliminato. Potrai crearlo di nuovo.</string>
|
||||
<string name="add_button">Aggiungi</string>
|
||||
<string name="add_relay_button">Aggiungi relay</string>
|
||||
<string name="add_relays_title">Aggiungi relay</string>
|
||||
<string name="button_cancel_and_delete_channel">Annulla ed elimina il canale</string>
|
||||
@@ -2844,7 +2821,6 @@
|
||||
<string name="channel_owner_count_plural">%1$d proprietari</string>
|
||||
<string name="channel_owners_contributors_count">%1$d proprietari e collaboratori</string>
|
||||
<string name="badge_supported_simplex">%1$s ha sostenuto SimpleX Chat. La targhetta è scaduta il %2$s.</string>
|
||||
<string name="settings_section_title_about">Informazioni</string>
|
||||
<string name="relay_status_acknowledged_roster">lista riconosciuta</string>
|
||||
<string name="webpage_code_footer">Aggiungi questo codice alla tua pagina web. Mostrerà l\'anteprima del tuo canale / gruppo.</string>
|
||||
<string name="advanced_options">Opzioni avanzate</string>
|
||||
@@ -2855,8 +2831,6 @@
|
||||
<string name="badge_unknown_key_title">La targhetta non può essere verificata</string>
|
||||
<string name="channel_webpage">Pagina web del canale</string>
|
||||
<string name="chat_data">Dati della chat</string>
|
||||
<string name="channel_name_requires_newer_app_version">La connessione tramite nome del canale richiede una versione dell\'app più recente.</string>
|
||||
<string name="contact_name_requires_newer_app_version">La connessione tramite nome del contatto richiede una versione dell\'app più recente.</string>
|
||||
<string name="settings_section_title_contact">Contatto</string>
|
||||
<string name="group_member_role_member_channel">collaboratore</string>
|
||||
<string name="copy_code">Copia codice</string>
|
||||
@@ -2868,7 +2842,6 @@
|
||||
<string name="webpage_url_footer">Verrà mostrato agli iscritti e usato per permettere il caricamento dell\'anteprima.</string>
|
||||
<string name="more_privacy">Più privacy</string>
|
||||
<string name="embed_only_your_page">Solo la tua pagina soprastante può mostrare l\'anteprima.</string>
|
||||
<string name="please_upgrade_the_app">Aggiorna l\'app.</string>
|
||||
<string name="badge_invested">%s ha investito nella raccolta fondi di SimpleX Chat.</string>
|
||||
<string name="badge_supports_simplex">%s sostiene SimpleX Chat.</string>
|
||||
<string name="group_member_role_observer_channel">iscritto</string>
|
||||
@@ -2877,8 +2850,6 @@
|
||||
<string name="member_role_will_be_changed_with_notification_channel">Il ruolo verrà cambiato in "%s". Verrà avvisato chiunque nel canale.</string>
|
||||
<string name="badge_unverified_desc">Non è stato possibile verificare questa targhetta e potrebbe non essere autentica.</string>
|
||||
<string name="group_link_requires_newer_version">Questo gruppo richiede una versione dell\'app più recente. Aggiorna l\'app per entrare.</string>
|
||||
<string name="unsupported_channel_name">Nome del canale non supportato</string>
|
||||
<string name="unsupported_contact_name">Nome del contatto non supportato</string>
|
||||
<string name="badge_unverified_title">Targhetta non verificata</string>
|
||||
<string name="relays_no_web_support">I relay di chat usati non supportano le pagine web.</string>
|
||||
<string name="webpage_code">Codice pagina web</string>
|
||||
@@ -2893,7 +2864,6 @@
|
||||
<string name="simplex_name_resolver_error_desc">Errore del risolutore: %1$s</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">Il server %1$s non supporta la risoluzione dei nomi. Configura i server o usa un link di connessione.</string>
|
||||
<string name="set_simplex_name">Imposta nome SimpleX</string>
|
||||
<string name="simplex_name">Nome SimpleX</string>
|
||||
<string name="simplex_name_error">Errore del nome SimpleX</string>
|
||||
<string name="simplex_name_not_verified">Nome SimpleX non verificato</string>
|
||||
<string name="simplex_name_no_valid_link_desc">Il nome SimpleX %1$s è registrato, ma non ha alcun link valido.</string>
|
||||
|
||||
@@ -1738,7 +1738,6 @@
|
||||
<string name="audio_device_earpiece">אוזניות</string>
|
||||
<string name="settings_section_title_user_theme">ערכת נושא לפרופיל</string>
|
||||
<string name="settings_section_title_chat_colors">צבעי הצא\'ט</string>
|
||||
<string name="chat_list_always_visible">הצג רשימת שיחות בחלון חדש</string>
|
||||
<string name="info_row_file_status">מצב הקובץ</string>
|
||||
<string name="info_row_message_status">סטטוס הודעה</string>
|
||||
<string name="share_text_file_status">מצב הקובץ:%s</string>
|
||||
@@ -2000,7 +1999,6 @@
|
||||
<string name="v6_3_organize_chat_lists_descr">אל תחמיץ הודעות חשובות.</string>
|
||||
<string name="v6_0_upgrade_app_descr">הורד גרסאות חדשות מ GitHub.</string>
|
||||
<string name="report_reason_profile">פרופיל לא הולם</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">לפרטיות מטא דאטא טובה יותר.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">אפשר Flux בהגדרות רשת ושרתים בשביל לשפר את הפרטיות של המטא דאטא</string>
|
||||
<string name="compose_forward_messages_n">מעביר %1$s הודעות</string>
|
||||
<string name="error_reading_passphrase">שגיאה בקריאת משפט-סיסמה של מסד נתונים</string>
|
||||
@@ -2123,7 +2121,6 @@
|
||||
<string name="onboarding_conditions_private_chats_not_accessible">צ\'אטים פרטיים, קבוצות ואנשי הקשר שלך אינם נגישים למפעילי השרת.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">לדוגמה, אם איש הקשר שלך מקבל הודעות דרך שרת SimpleX Chat, האפליקציה שלך תעביר אותן דרך שרת Flux.</string>
|
||||
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">מדיניות פרטיות ותנאי שימוש.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">ניתן להגדיר מפעילים בהגדרות רשת ושרתים.</string>
|
||||
<string name="privacy_chat_list_open_links_no">לא</string>
|
||||
<string name="settings_message_shape_tail">זנב לבועה</string>
|
||||
<string name="chat_bottom_bar">סרגל כלים נגיש לצ\'אט</string>
|
||||
|
||||
@@ -1821,7 +1821,6 @@
|
||||
\n*ネットワークとサーバー*設定から有効にして下さい。</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">いいえ</string>
|
||||
<string name="network_smp_proxy_fallback_allow">はい</string>
|
||||
<string name="update_network_smp_proxy_mode_question">メッセージルーティングモード</string>
|
||||
<string name="appearance_font_size">フォントサイズ</string>
|
||||
<string name="app_check_for_updates_beta">ベータ</string>
|
||||
<string name="app_check_for_updates_notice_title">アップデートを確認</string>
|
||||
@@ -1847,7 +1846,6 @@
|
||||
<string name="all_users">全てのプロフィール</string>
|
||||
<string name="info_view_call_button">通話</string>
|
||||
<string name="confirm_delete_contact_question">連絡先の削除を確認しますか?</string>
|
||||
<string name="info_view_connect_button">接続</string>
|
||||
<string name="delete_contact_cannot_undo_warning">連絡先が削除されます - この操作は取り消せません!</string>
|
||||
<string name="switching_profile_error_title">プロフィールの切り替えエラー</string>
|
||||
<string name="privacy_media_blur_radius">メディアのぼかし</string>
|
||||
@@ -1965,7 +1963,6 @@
|
||||
<string name="servers_info_missing">情報がありません、リロードしてください</string>
|
||||
<string name="smp_server">SMPサーバ</string>
|
||||
<string name="info_view_message_button">メッセージ</string>
|
||||
<string name="operator_website">ウェブサイト</string>
|
||||
<string name="info_view_video_button">ビデオ</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">設定画面からサーバを構成できます。</string>
|
||||
<string name="operator_open_changes">改定履歴を開く</string>
|
||||
@@ -1995,7 +1992,6 @@
|
||||
<string name="onboarding_notifications_mode_off_desc_short">バックグラウンドサービスを使用しません</string>
|
||||
<string name="accept_conditions">利用条件の承諾</string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s):既にオペレータ <b>%s</b> の利用条件に承諾しています。]]></string>
|
||||
<string name="address_creation_instruction">後で作成する場合はメニューから「SimpleXのアドレスを作成」を選択してください。</string>
|
||||
<string name="onboarding_network_about_operators">運営者について</string>
|
||||
<string name="chat_item_ttl_year">1年</string>
|
||||
<string name="group_reports_active_one">1 件のレポート</string>
|
||||
@@ -2053,7 +2049,6 @@
|
||||
<string name="content_filter_links">リンク</string>
|
||||
<string name="content_filter_all_messages">すべて</string>
|
||||
<string name="content_filter_voice_messages">音声メッセージ</string>
|
||||
<string name="content_filter_menu_item">フィルター</string>
|
||||
<string name="accept_pending_member_alert_confirmation_as_member">メンバーとして承認する</string>
|
||||
<string name="accept_pending_member_alert_confirmation_as_observer">オブザーバーとして承認する</string>
|
||||
<string name="blocking_reason_spam">スパム</string>
|
||||
@@ -2097,11 +2092,6 @@
|
||||
<string name="file_not_approved_descr">Tor または VPN を使用しない場合、あなたのIPアドレスはこれらのXFTPリレーに表示されます:\n%1$s。</string>
|
||||
<string name="unsupported_connection_link">サポートされていない接続リンク</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">このリンクには新しいバージョンのアプリが必要です。アプリをアップグレードするか、互換性のあるリンクを送るよう連絡先に依頼してください。</string>
|
||||
<string name="unsupported_channel_name">サポートされていないチャンネル名</string>
|
||||
<string name="unsupported_contact_name">サポートされていない連絡先名</string>
|
||||
<string name="channel_name_requires_newer_app_version">チャンネル名による接続には新しいバージョンのアプリが必要です。</string>
|
||||
<string name="contact_name_requires_newer_app_version">連絡先名による接続には新しいバージョンのアプリが必要です。</string>
|
||||
<string name="please_upgrade_the_app">アプリをアップグレードしてください。</string>
|
||||
<string name="channel_temporarily_unavailable">チャンネルは一時的に利用できません</string>
|
||||
<string name="channel_no_active_relays_try_later">チャンネルにアクティブなリレーがありません。後でもう一度参加をお試しください。</string>
|
||||
<string name="app_update_required">アプリの更新が必要です</string>
|
||||
@@ -2126,7 +2116,6 @@
|
||||
<string name="report_archive_alert_title">報告をアーカイブしますか?</string>
|
||||
<string name="report_archive_alert_title_nth">%d件の報告をアーカイブしますか?</string>
|
||||
<string name="report_archive_alert_title_all">すべての報告をアーカイブしますか?</string>
|
||||
<string name="report_archive_alert_desc">報告はあなたのためにアーカイブされます。</string>
|
||||
<string name="report_archive_alert_desc_all">すべての報告があなたのためにアーカイブされます。</string>
|
||||
<string name="report_archive_for_me">自分のみ</string>
|
||||
<string name="report_archive_for_all_moderators">すべてのモデレーター向け</string>
|
||||
@@ -2265,7 +2254,6 @@
|
||||
<string name="change_automatic_chat_deletion_message">この操作は元に戻せません - このチャットで選択した時点より前に送受信したメッセージは削除されます。</string>
|
||||
<string name="disable_automatic_deletion">メッセージ削除を無効にする</string>
|
||||
<string name="chat_ttl_options_footer">デバイスからチャットメッセージを削除します。</string>
|
||||
<string name="info_view_open_button">開く</string>
|
||||
<string name="info_view_search_button">検索</string>
|
||||
<string name="keep_conversation">会話を保持</string>
|
||||
<string name="only_delete_conversation">会話のみ削除</string>
|
||||
@@ -2281,7 +2269,6 @@
|
||||
<string name="the_sender_will_not_be_notified">送信者には通知されません。</string>
|
||||
<string name="member_is_deleted_cant_accept_request">メンバーが削除されています - リクエストを承諾できません</string>
|
||||
<string name="mute_all_chat">すべてミュート</string>
|
||||
<string name="unread_mentions">未読のメンション</string>
|
||||
<string name="change_list">リストを変更</string>
|
||||
<string name="duplicated_list_error">リスト名と絵文字はすべてのリストで異なる必要があります。</string>
|
||||
<string name="change_order_chat_list_menu_action">順序を変更</string>
|
||||
@@ -2321,7 +2308,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow_protected">IPが隠されている場合</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">あなたまたは宛先のサーバがプライベートルーティングをサポートしていない場合、メッセージを直接送信します。</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">IPアドレスが保護されており、かつあなたまたは宛先のサーバがプライベートルーティングをサポートしていない場合、メッセージを直接送信します。</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">メッセージルーティングのフォールバック</string>
|
||||
<string name="private_routing_explanation">IPアドレスを保護するため、プライベートルーティングはあなたのSMPサーバを使用してメッセージを配信します。</string>
|
||||
<string name="network_smp_web_port_section_title">メッセージング用のTCPポート</string>
|
||||
<string name="network_smp_web_port_toggle">Webポートを使用</string>
|
||||
@@ -2342,7 +2328,6 @@
|
||||
<string name="app_check_for_updates_notice_desc">新しいリリースの通知を受け取るには、安定版またはベータ版の定期チェックをオンにしてください。</string>
|
||||
<string name="deprecated_options_section">非推奨のオプション</string>
|
||||
<string name="prefs_error_saving_settings">設定の保存エラー</string>
|
||||
<string name="sent_to_your_contact_after_connection">接続後に連絡先に送信されます。</string>
|
||||
<string name="address_welcome_message">ウェルカムメッセージ</string>
|
||||
<string name="or_to_share_privately">またはプライベートに共有する場合</string>
|
||||
<string name="simplex_address_or_1_time_link">SimpleXアドレスまたはワンタイムリンク?</string>
|
||||
@@ -2389,12 +2374,7 @@
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">アプリは会話ごとに異なる運営者を使用することで、あなたのプライバシーを保護します。</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">複数の運営者が有効な場合、どの運営者も誰が誰と通信しているかを知るためのメタデータを持ちません。</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">例えば、あなたの連絡先がSimpleX Chatのサーバ経由でメッセージを受信する場合、あなたのアプリはFluxのサーバ経由でそれらを配信します。</string>
|
||||
<string name="onboarding_select_network_operators_to_use">使用するネットワーク運営者を選択してください。</string>
|
||||
<string name="how_it_helps_privacy">プライバシーにどう役立つか</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">有効な運営者の条件は30日後に承諾されます。</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">運営者は「ネットワークとサーバ」設定で構成できます。</string>
|
||||
<string name="onboarding_network_operators_review_later">後で確認</string>
|
||||
<string name="onboarding_network_operators_update">更新</string>
|
||||
<string name="onboarding_your_network">あなたのネットワーク</string>
|
||||
<string name="onboarding_network_routers_cannot_know">ネットワークのルーターは\n誰が誰と話しているかを知ることができません</string>
|
||||
<string name="onboarding_configure_routers">ルーターを設定</string>
|
||||
@@ -2411,7 +2391,6 @@
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">アプリは不明なファイルサーバからのダウンロードの確認を求めます(.onion の場合、またはSOCKSプロキシが有効な場合を除く)。</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor または VPN を使用しない場合、あなたのIPアドレスはファイルサーバに表示されます。</string>
|
||||
<string name="sanitize_links_toggle">リンクのトラッキングを削除</string>
|
||||
<string name="this_setting_is_for_your_current_profile">この設定は現在のプロフィールに適用されます</string>
|
||||
<string name="privacy_chat_list_open_links">チャット一覧からリンクを開く</string>
|
||||
<string name="privacy_chat_list_open_links_yes">はい</string>
|
||||
<string name="privacy_chat_list_open_links_no">いいえ</string>
|
||||
@@ -2421,7 +2400,6 @@
|
||||
<string name="privacy_chat_list_open_full_web_link">完全なリンクを開く</string>
|
||||
<string name="privacy_chat_list_open_clean_web_link">クリーンなリンクを開く</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">グループからの連絡先リクエスト</string>
|
||||
<string name="remote_hosts_section">リモートのモバイル端末</string>
|
||||
<string name="chat_item_ttl_default">デフォルト(%s)</string>
|
||||
<string name="chat_database_exported_save">エクスポートしたアーカイブを保存できます。</string>
|
||||
<string name="chat_database_exported_migrate">エクスポートしたデータベースを移行できます。</string>
|
||||
@@ -2433,7 +2411,6 @@
|
||||
<string name="chat_bottom_bar">手の届くチャットツールバー</string>
|
||||
<string name="one_hand_ui_bottom_bar">下部のバー</string>
|
||||
<string name="one_hand_ui_top_bar">上部のバー</string>
|
||||
<string name="chat_list_always_visible">新しいウィンドウでチャット一覧を表示</string>
|
||||
<string name="down_migration_warning_chat_relays">チャンネルに参加または作成した場合、それらは恒久的に機能しなくなります。</string>
|
||||
<string name="leave_channel_question">チャンネルから退出しますか?</string>
|
||||
<string name="leave_chat_question">チャットから退出しますか?</string>
|
||||
@@ -2475,7 +2452,6 @@
|
||||
<string name="only_chat_owners_can_change_prefs">設定を変更できるのはチャットのオーナーだけです。</string>
|
||||
<string name="action_button_channel_link">リンク</string>
|
||||
<string name="button_support_chat">管理者とのチャット</string>
|
||||
<string name="button_channel_members">チャンネルのメンバー</string>
|
||||
<string name="button_channel_relays">チャットリレー</string>
|
||||
<string name="button_remove_subscriber_question">購読者を削除しますか?</string>
|
||||
<string name="button_remove_members_question">メンバーを削除しますか?</string>
|
||||
@@ -2616,7 +2592,6 @@
|
||||
<string name="v6_2_network_decentralization">ネットワークの分散化</string>
|
||||
<string name="v6_2_network_decentralization_descr">アプリに2番目のプリセット運営者が登場!</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="v6_2_improved_chat_navigation_descr">- 最初の未読メッセージでチャットを開きます。\n- 引用されたメッセージにジャンプします。</string>
|
||||
<string name="v6_2_business_chats">ビジネスチャット</string>
|
||||
@@ -2801,7 +2776,6 @@
|
||||
<string name="your_profile_shared_with_channel_relays">あなたのプロフィール %1$s はチャンネルのリレーと購読者に共有されます。\nリレーはチャンネルのメッセージにアクセスできます。</string>
|
||||
<string name="configure_relays">リレーを構成</string>
|
||||
<string name="relay_status_failed">失敗</string>
|
||||
<string name="add_button">追加</string>
|
||||
<string name="add_relay_button">リレーを追加</string>
|
||||
<string name="add_relays_title">リレーを追加</string>
|
||||
<string name="no_available_relays">利用可能なリレーがありません</string>
|
||||
@@ -2817,10 +2791,8 @@
|
||||
<string name="relay_address_alert_title">リレーアドレス</string>
|
||||
<string name="relay_address_alert_message">これはチャットリレーのアドレスであり、接続には使用できません。</string>
|
||||
<string name="connect_plan_open_channel">チャンネルを開く</string>
|
||||
<string name="connect_plan_open_new_channel">新しいチャンネルを開く</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel">あなたのチャンネル</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[これはチャンネル <b>%1$s</b> 用のあなたのリンクです!]]></string>
|
||||
<string name="error_opening_channel">チャンネルを開く際のエラー</string>
|
||||
<string name="unblock_subscriber_for_all_question">全員に対して購読者のブロックを解除しますか?</string>
|
||||
<string name="link_previews_alert_title">リンクプレビューを有効にしますか?</string>
|
||||
<string name="link_previews_alert_desc">リンクプレビューを送信すると、あなたのIPアドレスがWebサイトに知られる可能性があります。これは後でプライバシー設定で変更できます。</string>
|
||||
|
||||
@@ -1080,7 +1080,6 @@
|
||||
<string name="migrate_from_device_confirm_upload">업로드 확인</string>
|
||||
<string name="connected_to_mobile">모바일에 연결</string>
|
||||
<string name="connected_mobile">연결된 모바일</string>
|
||||
<string name="info_view_connect_button">연결</string>
|
||||
<string name="cant_send_message_to_member_alert_title">그룹 멤버에게 메시지를 보낼 수 없음</string>
|
||||
<string name="receipts_contacts_title_enable">전송 알림을 활성화하시겠습니까?</string>
|
||||
<string name="conn_event_ratchet_sync_ok">암호화 OK</string>
|
||||
@@ -1379,7 +1378,6 @@
|
||||
<string name="app_check_for_updates_installed_successfully_title">성공적으로 설치됨</string>
|
||||
<string name="app_check_for_updates_button_install">업데이트 설치</string>
|
||||
<string name="theme_colors_section_title">인터페이스 색상</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">메시지 라우팅 대체</string>
|
||||
<string name="group_members_can_add_message_reactions">그룹 멤버가 메시지 반응을 추가할 수 있습니다.</string>
|
||||
<string name="settings_section_title_message_shape">메시지 모양</string>
|
||||
<string name="message_queue_info">메시지 대기열 정보</string>
|
||||
@@ -1395,7 +1393,6 @@
|
||||
<string name="v4_3_improved_server_configuration">향상된 서버 구성</string>
|
||||
<string name="compose_message_placeholder">메시지</string>
|
||||
<string name="message_servers">메시지 서버</string>
|
||||
<string name="update_network_smp_proxy_mode_question">메시지 라우팅 모드</string>
|
||||
<string name="email_invite_body">안녕하세요!\nSimpleX Chat 초대장이 도착했습니다: %s</string>
|
||||
<string name="agent_internal_error_title">내부 오류</string>
|
||||
<string name="invalid_name">잘못된 이름입니다!</string>
|
||||
@@ -1471,7 +1468,6 @@
|
||||
<string name="v6_2_network_decentralization_enable_flux">Flux 활성화</string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[모든 메시지와 파일은 쪽지에서 포스트 퀀텀 보안을 사용하여 <b>종단 간 암호화</b>로 전송됩니다.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc_short">앱이 항상 백그라운드에서 실행</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">활성화된 운영자에 대한 약관은 30일 후에 수락됩니다.</string>
|
||||
<string name="button_add_team_members">팀 멤버 추가하기</string>
|
||||
<string name="button_add_friends">친구 추가</string>
|
||||
<string name="delete_chat_for_all_members_cannot_undo_warning">모든 멤버에게서 채팅이 삭제됩니다 - 이 결정은 되돌릴 수 없습니다!</string>
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<string name="unmute_chat">Bêdengkirinê betal bike</string>
|
||||
<string name="favorite_chat">Bike favorît</string>
|
||||
<string name="unfavorite_chat">Ji favorîtan derxe</string>
|
||||
<string name="unread_mentions">Behsên nexwendî</string>
|
||||
<string name="create_list">Lîste çêke</string>
|
||||
<string name="add_to_list">Li lîstê zêde bike</string>
|
||||
<string name="change_list">Lîstê biguhere</string>
|
||||
@@ -345,7 +344,6 @@
|
||||
<string name="language_system">Sîstem</string>
|
||||
<string name="system_mode_toast">Moda sîstemê</string>
|
||||
<string name="settings_message_shape_tail">Terî/Dûvik</string>
|
||||
<string name="address_creation_instruction">Pêl Adresa SimpleXê çêke di meniwê de ji bo ko tu dûvre çêkî.</string>
|
||||
<string name="chat_banner_join_group">Pêl Bikeve komê bike</string>
|
||||
<string name="join_group_button">Bikeviyê</string>
|
||||
<string name="compose_view_join_group">Bikeve komê</string>
|
||||
@@ -603,7 +601,6 @@
|
||||
<string name="network_type_ethernet">Etherneta bi qeblo</string>
|
||||
<string name="network_type_network_wifi">WiFi</string>
|
||||
<string name="whats_new">Çi yî nû heye</string>
|
||||
<string name="operator_website">Websîte</string>
|
||||
<string name="webrtc_ice_servers">Serverên WebRTC ICEyê</string>
|
||||
<string name="database_downgrade_warning">Hişyarî: hinek dataya te kare winda bibe!</string>
|
||||
<string name="contact_wants_to_connect_with_you">dixwaze bi te re bikeve danûstandinê!</string>
|
||||
|
||||
@@ -539,7 +539,6 @@
|
||||
<string name="empty_chat_profile_is_created">Tiek izveidots tukšs čata profils</string>
|
||||
<string name="if_you_enter_passcode_data_removed">Ja ievadīsiet paroli, dati tiks izdzēsti</string>
|
||||
<string name="set_passcode">Uzstādīt paroli</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Šis iestatījums ir paredzēts jūsu pašreizējam profilam</string>
|
||||
<string name="rcv_group_event_1_member_connected">Saņemts grupas notikums: 1 dalībnieks pievienojies</string>
|
||||
<string name="rcv_group_event_2_members_connected">Saņemts grupas notikums: 2 dalībnieki pievienojušies</string>
|
||||
<string name="rcv_group_event_3_members_connected">Saņemts grupas notikums: 3 dalībnieki pievienojušies</string>
|
||||
@@ -1022,7 +1021,6 @@
|
||||
<string name="auth_open_chat_profiles">Atvērt sarunu profilus</string>
|
||||
<string name="report_archive_alert_title_nth">Ziņojumu arhīvs %dth</string>
|
||||
<string name="report_archive_alert_title_all">Ziņojumu arhīvs visi</string>
|
||||
<string name="report_archive_alert_desc">Ziņojumu arhīvs</string>
|
||||
<string name="report_archive_alert_desc_all">Ziņojumu arhīvs apraksts visi</string>
|
||||
<string name="report_archive_for_me">Ziņojumu arhīvs man</string>
|
||||
<string name="report_archive_for_all_moderators">Ziņojumu arhīvs visiem moderātoriem</string>
|
||||
@@ -1134,7 +1132,6 @@
|
||||
<string name="contact_should_accept">Kontaktam jāpieņem</string>
|
||||
<string name="connect_with_contact_name_question">Savienoties ar kontaktu %s?</string>
|
||||
<string name="search_or_paste_simplex_link">Meklējiet vai ielīmējiet simplex saiti</string>
|
||||
<string name="address_creation_instruction">Adreses izveides instrukcija</string>
|
||||
<string name="no_selected_chat">Nav izvēlēta saruna</string>
|
||||
<string name="selected_chat_items_nothing_selected">Izvēlēto sarunu vienības nav izvēlētas</string>
|
||||
<string name="selected_chat_items_selected_n">Izvēlēto sarunu vienības izvēlētas %d</string>
|
||||
@@ -1167,8 +1164,6 @@
|
||||
<string name="change_automatic_chat_deletion_message">Mainīt automātisko sarunu dzēšanu</string>
|
||||
<string name="disable_automatic_deletion">Atspējot automātisko dzēšanu</string>
|
||||
<string name="chat_ttl_options_footer">Sarunu laika ierobežojuma opciju apakšdaļa</string>
|
||||
<string name="info_view_connect_button">Skatīt savienojumu</string>
|
||||
<string name="info_view_open_button">Skatīt atvērt</string>
|
||||
<string name="info_view_message_button">Skatīt</string>
|
||||
<string name="info_view_call_button">Skatīt zvanu</string>
|
||||
<string name="info_view_search_button">Skatīt meklēšanu</string>
|
||||
@@ -1273,7 +1268,6 @@
|
||||
<string name="unmute_chat">Atjaunot sarunu</string>
|
||||
<string name="favorite_chat">Atzīmēt kā iecienītu</string>
|
||||
<string name="unfavorite_chat">Noņemt no iecienītajiem</string>
|
||||
<string name="unread_mentions">Neizlasītie minējumi</string>
|
||||
<string name="create_list">Izveidot sarakstu</string>
|
||||
<string name="add_to_list">Pievienot sarakstam</string>
|
||||
<string name="change_list">Mainīt sarakstu</string>
|
||||
@@ -1390,7 +1384,6 @@
|
||||
<string name="network_smp_proxy_mode_unknown_description">Tīkla smp proxy režīms nezināms</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Tīkla smp proxy režīms neaizsargāts</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Tīkla smp proxy režīms nekad</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Atjaunināt tīkla smp proxy režīmu?</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Tīkla smp proxy rezerves atļaut samazināšanu</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Tīkla smp proxy rezerves atļaut</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Tīkla smp proxy rezerves atļaut aizsargātu</string>
|
||||
@@ -1398,7 +1391,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow_description">Tīkla smp proxy rezerves atļaut</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Tīkla smp proxy rezerves atļaut aizsargātu</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Tīkla smp proxy rezerves aizliegt</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Atjaunināt tīkla smp proxy rezerves?</string>
|
||||
<string name="private_routing_show_message_status">Privātā maršrutēšana rādīt</string>
|
||||
<string name="private_routing_explanation">Privātā maršrutēšana skaidrojums</string>
|
||||
<string name="network_smp_web_port_section_title">Tīkla smp tīmekļa ports</string>
|
||||
@@ -1461,7 +1453,6 @@
|
||||
<string name="stop_sharing_address">Pārtraukt adreses kopīgošanu</string>
|
||||
<string name="stop_sharing">Pārtraukt kopīgošanu</string>
|
||||
<string name="auto_accept_contact">Automātiski pieņemt kontaktus</string>
|
||||
<string name="sent_to_your_contact_after_connection">Nosūtīts jūsu kontaktam pēc savienojuma izveides</string>
|
||||
<string name="address_welcome_message">Sveiciena ziņa</string>
|
||||
<string name="enter_welcome_message_optional">Ievadiet sveiciena ziņu (pēc izvēles)</string>
|
||||
<string name="save_settings_question">Saglabāt iestatījumus?</string>
|
||||
@@ -1598,13 +1589,8 @@
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Ievada tīkla operatori nevar redzēt, kas ar ko runā</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Ievada tīkla operatori lietotne izmantos maršrutēšanai</string>
|
||||
<string name="onboarding_network_about_operators">Ievada tīkls par operatoriem</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Ievada izvēlēties tīkla operatorus, ko izmantot</string>
|
||||
<string name="how_it_helps_privacy">Kā tas palīdz privātumam</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Ievada tīkla operatori konfigurēt caur iestatījumiem</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Ievada tīkla operatori nosacījumi tiks pieņemti</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Ievada tīkla operatori nosacījumi, ko jūs varat konfigurēt</string>
|
||||
<string name="onboarding_network_operators_review_later">Ievada tīkla operatori pārskatīt vēlāk</string>
|
||||
<string name="onboarding_network_operators_update">Ievada tīkla operatori atjaunināt</string>
|
||||
<string name="onboarding_network_operators_continue">Ievada tīkla operatori turpināt</string>
|
||||
<string name="incoming_video_call">Ienākošais video zvans</string>
|
||||
<string name="incoming_audio_call">Ienākošais audio zvans</string>
|
||||
@@ -1675,7 +1661,6 @@
|
||||
<string name="settings_section_title_use_from_desktop">Izmantot no darbvirsmas</string>
|
||||
<string name="your_chat_database">Jūsu sarunu datubāze</string>
|
||||
<string name="run_chat_section">Palaist sarunu</string>
|
||||
<string name="remote_hosts_section">Attālie hosti</string>
|
||||
<string name="chat_is_running">Saruna notiek</string>
|
||||
<string name="chat_is_stopped">Saruna ir apstājusies</string>
|
||||
<string name="chat_database_section">Sarunu datubāze</string>
|
||||
@@ -1799,7 +1784,6 @@
|
||||
<string name="chat_bottom_bar">Sarunu apakšējā josla</string>
|
||||
<string name="one_hand_ui_change_instruction">Vienas rokas saskarnes maiņas instrukcija</string>
|
||||
<string name="terminal_always_visible">Terminālis vienmēr redzams</string>
|
||||
<string name="chat_list_always_visible">Sarunu saraksts vienmēr redzams</string>
|
||||
<string name="invalid_migration_confirmation">Nederīga migrācijas apstiprināšana</string>
|
||||
<string name="upgrade_and_open_chat">Jaunināt un atvērt sarunu</string>
|
||||
<string name="downgrade_and_open_chat">Samazināt un atvērt sarunu</string>
|
||||
@@ -1994,7 +1978,6 @@
|
||||
<string name="operator">Operators</string>
|
||||
<string name="operator_servers_title">Operatora serveri</string>
|
||||
<string name="operator_info_title">Operators</string>
|
||||
<string name="operator_website">Operatora mājaslapa</string>
|
||||
<string name="operator_conditions_accepted_on">Operatora nosacījumi pieņemti</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Operatora nosacījumi tiks pieņemti</string>
|
||||
<string name="operator_use_operator_toggle_description">Operatora izmantošanas slēdzis</string>
|
||||
@@ -2317,7 +2300,6 @@
|
||||
<string name="v6_2_network_decentralization">Tīkla decentralizācija</string>
|
||||
<string name="v6_2_network_decentralization_descr">Decentralizējiet tīklu, lai uzlabotu drošību.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Tīkla decentralizācija, iespējot plūsmu</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">Tīkla decentralizācija, iespējot plūsmu iemesls</string>
|
||||
<string name="v6_2_improved_chat_navigation">Uzlabota sarunu navigācija</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">Vieglāka navigācija sarunās.</string>
|
||||
<string name="v6_2_business_chats">Uzņēmumu sarunas</string>
|
||||
|
||||
@@ -1772,7 +1772,6 @@
|
||||
<string name="network_smp_proxy_mode_always_description">Gebruik altijd privéroutering.</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Gebruik privéroutering met onbekende servers.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Gebruik GEEN privéroutering.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Berichtrouteringsmodus</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Downgraden toestaan</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Wanneer IP verborgen is</string>
|
||||
@@ -1782,7 +1781,6 @@
|
||||
<string name="private_routing_show_message_status">Toon berichtstatus</string>
|
||||
<string name="private_routing_explanation">Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Terugval op berichtroutering</string>
|
||||
<string name="settings_section_title_private_message_routing">Privébericht routing</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt.</string>
|
||||
<string name="file_not_approved_title">Onbekende servers!</string>
|
||||
@@ -1835,7 +1833,6 @@
|
||||
<string name="color_mode_system">Systeem</string>
|
||||
<string name="color_wallpaper_background">Wallpaper achtergrond</string>
|
||||
<string name="chat_theme_set_default_theme">Stel het standaard thema in</string>
|
||||
<string name="chat_list_always_visible">Toon chat-lijst in nieuw venster</string>
|
||||
<string name="message_queue_info_none">geen</string>
|
||||
<string name="info_row_debug_delivery">Foutopsporing bezorging</string>
|
||||
<string name="message_queue_info">Informatie over berichtenwachtrij</string>
|
||||
@@ -1999,13 +1996,11 @@
|
||||
<string name="compose_message_placeholder">Bericht</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Berichten worden gemarkeerd voor verwijdering. De ontvanger(s) kunnen deze berichten onthullen.</string>
|
||||
<string name="selected_chat_items_nothing_selected">Niets geselecteerd</string>
|
||||
<string name="info_view_connect_button">verbinden</string>
|
||||
<string name="contact_deleted">Contact verwijderd!</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="keep_conversation">Behoud het gesprek</string>
|
||||
<string name="info_view_message_button">bericht</string>
|
||||
<string name="only_delete_conversation">Alleen conversatie verwijderen</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="paste_link">Plak de link</string>
|
||||
<string name="action_button_add_members">Uitnodiging</string>
|
||||
<string name="one_hand_ui">Bereikbare app-toolbars</string>
|
||||
@@ -2129,7 +2124,6 @@
|
||||
<string name="connection_error_quota">Niet afgeleverde berichten</string>
|
||||
<string name="connection_error_quota_desc">De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Dit bericht is verwijderd of nog niet ontvangen.</string>
|
||||
<string name="address_creation_instruction">Tik op SimpleX-adres maken in het menu om het later te maken.</string>
|
||||
<string name="share_address_publicly">Adres openbaar delen</string>
|
||||
<string name="share_1_time_link_with_a_friend">Deel eenmalig een link met een vriend</string>
|
||||
<string name="share_simplex_address_on_social_media">Deel het SimpleX-adres op sociale media.</string>
|
||||
@@ -2143,13 +2137,8 @@
|
||||
<string name="address_settings">Adres instellingen</string>
|
||||
<string name="create_1_time_link">Eenmalige link maken</string>
|
||||
<string name="onboarding_choose_server_operators">Serverbeheerders</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd.</string>
|
||||
<string name="onboarding_network_operators">Netwerkbeheerders</string>
|
||||
<string name="onboarding_network_operators_review_later">Later beoordelen</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Selecteer welke netwerkoperators u wilt gebruiken.</string>
|
||||
<string name="onboarding_network_operators_update">Update</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">De app beschermt uw privacy door in elk gesprek verschillende operators te gebruiken.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">U kunt operators configureren in Netwerk- en serverinstellingen.</string>
|
||||
<string name="onboarding_network_operators_continue">Doorgaan</string>
|
||||
<string name="operator_review_conditions">Voorwaarden bekijken</string>
|
||||
<string name="your_servers">Uw servers</string>
|
||||
@@ -2164,7 +2153,6 @@
|
||||
<string name="operator_servers_title">%s servers</string>
|
||||
<string name="use_servers_of_operator_x">Gebruik %s</string>
|
||||
<string name="operator_use_operator_toggle_description">Gebruik servers</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Voorwaarden worden geaccepteerd voor operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_also_apply"><![CDATA[Deze voorwaarden zijn ook van toepassing op: <b>%s</b>.]]></string>
|
||||
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Om de servers van <b>%s</b> te gebruiken, moet u de gebruiksvoorwaarden accepteren.]]></string>
|
||||
@@ -2180,7 +2168,6 @@
|
||||
<string name="operator_server_alert_title">Operatorserver</string>
|
||||
<string name="server_added_to_operator__name">Server toegevoegd aan operator %s.</string>
|
||||
<string name="appearance_in_app_bars_alpha">Transparantie</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">voor betere privacy van metagegevens.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Verbeterde chatnavigatie</string>
|
||||
<string name="v6_2_network_decentralization">Netwerk decentralisatie</string>
|
||||
<string name="v6_2_network_decentralization_descr">De tweede vooraf ingestelde operator in de app!</string>
|
||||
@@ -2208,7 +2195,6 @@
|
||||
<string name="onboarding_network_operators_configure_via_settings">U kunt servers configureren via instellingen.</string>
|
||||
<string name="chat_archive">Of importeer archiefbestand</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Directe berichten tussen leden zijn in deze chat niet toegestaan.</string>
|
||||
<string name="remote_hosts_section">Externe mobiele telefoons</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi-apparaten</b>: schakel Automatisch starten in de systeeminstellingen in om meldingen te laten werken.]]></string>
|
||||
<string name="maximum_message_size_title">Bericht is te groot!</string>
|
||||
<string name="maximum_message_size_reached_text">Verklein het bericht en verstuur het opnieuw.</string>
|
||||
@@ -2299,7 +2285,6 @@
|
||||
<string name="report_archive_alert_title">Rapport archiveren?</string>
|
||||
<string name="report_verb">rapporteren</string>
|
||||
<string name="report_reason_alert_title">Reden melding?</string>
|
||||
<string name="report_archive_alert_desc">Het rapport wordt voor u gearchiveerd.</string>
|
||||
<string name="report_compose_reason_header_other">Anders melden: alleen groepsmoderators kunnen het zien.</string>
|
||||
<string name="report_compose_reason_header_spam">Spam melden: alleen groepsmoderators kunnen het zien.</string>
|
||||
<string name="report_compose_reason_header_community">Rapporteer overtreding: alleen groepsmoderators kunnen dit zien.</string>
|
||||
@@ -2334,7 +2319,6 @@
|
||||
<string name="network_smp_web_port_toggle">Gebruik een webpoort</string>
|
||||
<string name="network_smp_web_port_footer">Gebruik TCP-poort %1$s wanneer er geen poort is opgegeven.</string>
|
||||
<string name="mute_all_chat">Alles dempen</string>
|
||||
<string name="unread_mentions">Ongelezen vermeldingen</string>
|
||||
<string name="max_group_mentions_per_message_reached">Je kunt maximaal %1$s leden per bericht vermelden!</string>
|
||||
<string name="enable_sending_member_reports">Hiermee kunt u berichten rapporteren aan moderators.</string>
|
||||
<string name="report_archive_alert_desc_all">Alle rapporten worden voor u gearchiveerd.</string>
|
||||
|
||||
@@ -1771,7 +1771,6 @@
|
||||
<string name="network_smp_proxy_mode_never">Nigdy</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Niezabezpieczony</string>
|
||||
<string name="network_smp_proxy_mode_never_description">NIE używaj prywatnego trasowania.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Tryb trasowania wiadomości</string>
|
||||
<string name="network_smp_proxy_fallback_allow">Tak</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Nie</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected">Gdy IP ukryty</string>
|
||||
@@ -1781,7 +1780,6 @@
|
||||
<string name="private_routing_explanation">Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości.</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Nieznane serwery</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Używaj prywatnego trasowania z nieznanymi serwerami.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Rezerwowe trasowania wiadomości</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Prywatne trasowanie</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</string>
|
||||
@@ -1794,7 +1792,6 @@
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików.</string>
|
||||
<string name="settings_section_title_files">Pliki</string>
|
||||
<string name="settings_section_title_user_theme">Motyw profilu</string>
|
||||
<string name="chat_list_always_visible">Pokaż listę czatów w nowym oknie</string>
|
||||
<string name="dark_mode_colors">Kolory ciemnego trybu</string>
|
||||
<string name="color_mode_light">Jasny</string>
|
||||
<string name="color_received_quote">Otrzymano odpowiedź</string>
|
||||
@@ -1981,7 +1978,6 @@
|
||||
<string name="v6_0_increase_font_size">Zwiększ rozmiar czcionki.</string>
|
||||
<string name="info_view_message_button">wiadomość</string>
|
||||
<string name="compose_message_placeholder">Wiadomość</string>
|
||||
<string name="info_view_open_button">otwórz</string>
|
||||
<string name="info_view_search_button">szukaj</string>
|
||||
<string name="selected_chat_items_selected_n">Zaznaczono %d</string>
|
||||
<string name="info_view_video_button">wideo</string>
|
||||
@@ -2002,7 +1998,6 @@
|
||||
<string name="v6_0_chat_list_media">Odtwórz z listy czatów.</string>
|
||||
<string name="deleted_chats">Zarchiwizowane kontakty</string>
|
||||
<string name="info_view_call_button">zadzwoń</string>
|
||||
<string name="info_view_connect_button">połącz</string>
|
||||
<string name="v6_0_connection_servers_status_descr">Stan połączenia i serwerów.</string>
|
||||
<string name="create_address_button">Utwórz</string>
|
||||
<string name="error_parsing_uri_title">Nieprawidłowy link</string>
|
||||
@@ -2183,7 +2178,6 @@
|
||||
<string name="delete_chat_for_all_members_cannot_undo_warning">Czat zostanie usunięty dla wszystkich członków - nie można tego cofnąć!</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Warunki będą akceptowane w dniu: %s.</string>
|
||||
<string name="connection_security">Bezpieczeństwo połączenia</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Warunki będą akceptowane dla operatorów włączonych po 30 dniach.</string>
|
||||
<string name="change_order_chat_list_menu_action">Zmień kolejność</string>
|
||||
<string name="sync_connection_desc">Połączenie wymaga renegocjacji szyfrowania.</string>
|
||||
<string name="rcv_group_event_member_accepted">przyjął %1$s</string>
|
||||
@@ -2288,14 +2282,12 @@
|
||||
<string name="file_error_blocked">Plik jest zablokowany przez operatora serwera:\n%1$s.</string>
|
||||
<string name="content_filter_files">Pliki</string>
|
||||
<string name="files_prohibited_in_this_chat">Pliki i media są zabronione na tym czacie.</string>
|
||||
<string name="content_filter_menu_item">Filtr</string>
|
||||
<string name="proxy_destination_error_unknown_ca">Odcisk palca w docelowym serwerze nie pasuje do certyfikatu: %1$s.</string>
|
||||
<string name="smp_proxy_error_unknown_ca">Odcisk palca w adresie serwera nie pasuje do certyfikatu: %1$s.</string>
|
||||
<string name="network_error_unknown_ca">Odcisk palca w adresie serwera nie pasuje do certyfikatu: %1$s.</string>
|
||||
<string name="sync_connection_confirm">Napraw</string>
|
||||
<string name="sync_connection_question">Naprawić połączenie?</string>
|
||||
<string name="report_archive_for_all_moderators">Dla wszystkich moderatorów</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">Lepsza prywatność metadanych.</string>
|
||||
<string name="for_chat_profile">Dla profilu czatu %s:</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Na przykład, jeśli kontakt otrzyma wiadomości za pośrednictwem serwera czatu SimpleX, aplikacja dostarczy je za pośrednictwem serwera Flux.</string>
|
||||
<string name="report_archive_for_me">Dla mnie</string>
|
||||
@@ -2417,7 +2409,6 @@
|
||||
<string name="group_preview_rejected">odrzucono</string>
|
||||
<string name="group_member_status_rejected">odrzucono</string>
|
||||
<string name="reject_pending_member_alert_title">Odrzucić członka?</string>
|
||||
<string name="remote_hosts_section">Zdalne telefony komórkowe</string>
|
||||
<string name="remove_member_delete_messages_confirmation">Usuń i skasuj wiadomości</string>
|
||||
<string name="cant_send_message_mem_removed">usunięty z grupy</string>
|
||||
<string name="sanitize_links_toggle">Usuń śledzenie linków</string>
|
||||
@@ -2442,7 +2433,6 @@
|
||||
<string name="operator_review_conditions">Przejrzyj warunki</string>
|
||||
<string name="reviewed_by_admins">sprawdzone przez administratorów</string>
|
||||
<string name="v6_4_review_members">Przejrzyj członków grupy</string>
|
||||
<string name="onboarding_network_operators_review_later">Przejrzyj później</string>
|
||||
<string name="admission_stage_review">Przejrzyj członków</string>
|
||||
<string name="admission_stage_review_descr">Przejrzyj członków przed przyjęciem (pukanie).</string>
|
||||
<string name="save_admission_question">Zapisać ustawienia wstępu?</string>
|
||||
@@ -2452,13 +2442,11 @@
|
||||
<string name="placeholder_search_links">Szukaj linków</string>
|
||||
<string name="placeholder_search_videos">Szukaj wideo</string>
|
||||
<string name="placeholder_search_voice_messages">Szukaj wiadomości głosowych</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Wybierz operatora sieci</string>
|
||||
<string name="compose_view_send_contact_request_alert_question">Wysłać prośbę o kontakt?</string>
|
||||
<string name="v6_3_reports">Wyślij prywatne zgłoszenia</string>
|
||||
<string name="compose_view_send_request">Wyślij prośbę</string>
|
||||
<string name="compose_view_send_request_without_message">Wyślij prośbę bez wiadomości</string>
|
||||
<string name="v6_4_support_chat_descr">Wyślij swoją prywatną opinię do grup.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Wysłano do Twojego kontaktu po połączeniu.</string>
|
||||
<string name="server_added_to_operator__name">Serwer dodany do operatora %s.</string>
|
||||
<string name="error_server_operator_changed">Operator serwera został zmieniony.</string>
|
||||
<string name="onboarding_choose_server_operators">Operatorzy serwera</string>
|
||||
@@ -2484,7 +2472,6 @@
|
||||
<string name="chat_banner_connect_to_chat">Dotknij Połącz aby rozpocząć czat</string>
|
||||
<string name="chat_banner_send_request_to_connect">Dotknij Połącz, aby wysłać prośbę</string>
|
||||
<string name="chat_banner_connect_to_use_bot">Dotknij Połącz aby użyć bota</string>
|
||||
<string name="address_creation_instruction">Dotknij Stwórz adres SimpleX w menu aby utworzyć go później.</string>
|
||||
<string name="chat_banner_join_group">Dotknij Dołącz do grupy</string>
|
||||
<string name="network_option_tcp_connection_timeout_background">Przekroczono limit czasu połączenia TCP</string>
|
||||
<string name="network_smp_web_port_section_title">Port TCP dla wiadomości</string>
|
||||
@@ -2492,7 +2479,6 @@
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Aplikacja chroni Twoją prywatność, korzystając z różnych operatorów w każdej rozmowie.</string>
|
||||
<string name="connection_error_quota_desc">Połączenie osiągnęło limit niedostarczonych wiadomości, Twój kontakt może być offline.</string>
|
||||
<string name="share_group_profile_via_link_alert_text">Link będzie krótki, a profil grupowy zostanie udostępniony poprzez link.</string>
|
||||
<string name="report_archive_alert_desc">Raport zostanie dla Ciebie zarchiwizowany.</string>
|
||||
<string name="member_role_will_be_changed_with_notification_chat">Rola zostanie zmieniona na %s. Wszyscy uczestnicy czatu zostaną powiadomieni.</string>
|
||||
<string name="v6_2_network_decentralization_descr">Drugi predefiniowany operator w aplikacji!</string>
|
||||
<string name="the_sender_will_not_be_notified">Nadawca NIE zostanie poinformowany.</string>
|
||||
@@ -2500,7 +2486,6 @@
|
||||
<string name="change_automatic_chat_deletion_message">Tej akcji nie można cofnąć - wiadomości wysłane i otrzymane na tym czacie wcześniej niż wybrane zostaną usunięte.</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">Ten link wymaga nowszej wersji aplikacji. Zaktualizuj aplikację lub poproś osobę kontaktową o przesłanie kompatybilnego łącza.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Ta wiadomość została usunięta lub jeszcze nie otrzymana.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">To ustawienie jest dla Twojego obecnego profilu.</string>
|
||||
<string name="time_to_disappear_is_set_only_for_new_contacts">Czas zniknięcia jest ustawiony tylko dla nowych kontaktów.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Aby zabezpieczyć się przed wymianą łącza, możesz porównać kody bezpieczeństwa kontaktu.</string>
|
||||
<string name="operator_use_for_messages_receiving">Żeby odebrać</string>
|
||||
@@ -2510,9 +2495,7 @@
|
||||
<string name="appearance_in_app_bars_alpha">Przeźroczystość</string>
|
||||
<string name="unblock_members_for_all_question">Odblokować członków dla wszystkich?</string>
|
||||
<string name="connection_error_quota">Niedostarczone wiadomości</string>
|
||||
<string name="unread_mentions">Nieprzeczytane wzmianki</string>
|
||||
<string name="unsupported_connection_link">Nieobsługiwane łącze połączenia</string>
|
||||
<string name="onboarding_network_operators_update">Aktualizacja</string>
|
||||
<string name="operator_updated_conditions">Warunki aktualizacji</string>
|
||||
<string name="v6_4_1_short_address_update">Aktualizuj swój adres</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Upgrade</string>
|
||||
@@ -2532,14 +2515,12 @@
|
||||
<string name="view_conditions">Zobacz warunki</string>
|
||||
<string name="view_updated_conditions">Zobacz zaktualizowane warunki</string>
|
||||
<string name="content_filter_voice_messages">Wiadomości głosowe</string>
|
||||
<string name="operator_website">Strona Internetowa</string>
|
||||
<string name="address_welcome_message">Wiadomość powitalna</string>
|
||||
<string name="v6_4_1_welcome_contacts">Powitaj swoje kontakty</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Gdy włączony jest więcej niż jeden operator, żaden z nich nie ma metadanych pozwalających dowiedzieć się, kto się z kim komunikuje.</string>
|
||||
<string name="privacy_chat_list_open_links_yes">Tak</string>
|
||||
<string name="snd_group_event_member_accepted">zaakceptowałeś tego członka</string>
|
||||
<string name="not_connected_to_server_to_receive_messages_no_sub">Nie masz połączenia z serwerem używanym do odbierania wiadomości z tego połączenia (brak subskrypcji).</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Możesz skonfigurować operatorów w ustawieniach sieci i serwerów.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Serwery można skonfigurować w ustawieniach.</string>
|
||||
<string name="maximum_message_size_reached_forwarding">Możesz skopiować i zmniejszyć rozmiar wiadomości, aby ją wysłać.</string>
|
||||
<string name="max_group_mentions_per_message_reached">Możesz wzmiankować do %1$s członków na wiadomość!</string>
|
||||
|
||||
@@ -1648,7 +1648,6 @@
|
||||
<string name="privacy_media_blur_radius">Desfocar mídia</string>
|
||||
<string name="v6_0_privacy_blur">Borrar para melhor privacidade.</string>
|
||||
<string name="confirm_delete_contact_question">Confirmar exclusão do contato?</string>
|
||||
<string name="info_view_connect_button">conectar</string>
|
||||
<string name="delete_without_notification">Apagar sem notificar</string>
|
||||
<string name="color_primary_variant2">Destaque adicional 2</string>
|
||||
<string name="migrate_to_device_confirm_network_settings">Confirmar configurações de rede</string>
|
||||
@@ -1731,7 +1730,6 @@
|
||||
<string name="app_check_for_updates_button_skip">Pular essa versão</string>
|
||||
<string name="app_check_for_updates_button_download">Baixar %s (%s)</string>
|
||||
<string name="app_check_for_updates_canceled">Download da atualização cancelado</string>
|
||||
<string name="chat_list_always_visible">Mostrar lista de chats em nova janela</string>
|
||||
<string name="appearance_font_size">Tamanho da fonte</string>
|
||||
<string name="color_wallpaper_background">Fundo do papel de parede</string>
|
||||
<string name="color_wallpaper_tint">Tonalidade do papel de parede</string>
|
||||
@@ -1846,7 +1844,6 @@
|
||||
<string name="network_smp_proxy_mode_unknown">Servidores desconhecidos</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Desprotegido</string>
|
||||
<string name="network_smp_proxy_mode_never">Nunca</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Modo de roteamento de mensagens</string>
|
||||
<string name="permissions_required">Conceder permissões</string>
|
||||
<string name="audio_device_speaker">Alto falante</string>
|
||||
<string name="audio_device_wired_headphones">Headphones</string>
|
||||
@@ -1912,7 +1909,6 @@
|
||||
<string name="v5_8_private_routing_descr">Proteja seu endereço IP dos relays de mensagens escolhidos pelos seus contatos.\nAtive nas configurações de Rede e servidores.</string>
|
||||
<string name="migrate_from_device_start_chat">Iniciar o chat</string>
|
||||
<string name="toolbar_settings">Configurações</string>
|
||||
<string name="info_view_open_button">abrir</string>
|
||||
<string name="keep_conversation">Manter conversa</string>
|
||||
<string name="only_delete_conversation">Apenas apagar conversa</string>
|
||||
<string name="xftp_servers_other">Outros servidores XFTP</string>
|
||||
@@ -2019,7 +2015,6 @@
|
||||
<string name="contact_list_header_title">Seus contatos</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">NÃO envie mensagens diretamente, mesmo que o seu servidor ou o servidor de destino não suporte roteamento privado.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_description">Enviar mensagens diretamente quando o seu servidor ou o servidor de destino não for compatível com roteamento privado.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Retorno de roteamento de mensagens</string>
|
||||
<string name="private_routing_show_message_status">Mostrar status da mensagem</string>
|
||||
<string name="migrate_from_another_device">Migrar de outro dispositivo</string>
|
||||
<string name="share_text_message_status">Status da mensagem: %s</string>
|
||||
@@ -2170,7 +2165,6 @@
|
||||
<string name="operator_conditions_of_use">Condições de uso</string>
|
||||
<string name="error_adding_server">Erro ao adicionar servidor</string>
|
||||
<string name="v6_2_business_chats">Chats de empresas</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">para melhoria da privacidade de metadados.</string>
|
||||
<string name="v6_3_faster_deletion_of_groups">Exclusão mais rápida de grupos.</string>
|
||||
<string name="error_creating_chat_tags">Erro ao criar lista de chat</string>
|
||||
<string name="error_loading_chat_tags">Erro ao carregar lista de chats</string>
|
||||
@@ -2194,7 +2188,6 @@
|
||||
<string name="encryption_renegotiation_in_progress">Renegociação de criptografia em andamento.</string>
|
||||
<string name="delete_chat_list_menu_action">Apagar</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Clique no botão de informação perto do campo de endereço para permitir usar o microfone.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">As condições serão aceitas para operadores habilitados após 30 dias.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Por exemplo, se o seu contato receber mensagens por meio de um servidor SimpleX Chat, seu aplicativo as entregará por meio de um servidor Flux.</string>
|
||||
<string name="connection_not_ready">A conexão não está pronta.</string>
|
||||
<string name="errors_in_servers_configuration">Erros nas configurações de servidores.</string>
|
||||
@@ -2284,7 +2277,6 @@
|
||||
<string name="disable_sending_member_reports">Proibir a denúncia de mensagens aos moderadores.</string>
|
||||
<string name="report_compose_reason_header_illegal">Denunciar conteúdo: somente os moderadores do grupo poderão ver.</string>
|
||||
<string name="report_compose_reason_header_profile">Denunciar perfil de membro: somente moderadores do grupo poderão ver.</string>
|
||||
<string name="remote_hosts_section">Dispositivos móveis remotos</string>
|
||||
<string name="button_remove_members_question">Remover membros?</string>
|
||||
<string name="report_compose_reason_header_other">Denunciar outro: somente os moderadores do grupo poderão ver.</string>
|
||||
<string name="report_reason_alert_title">Qual é a razão da denúncia?</string>
|
||||
@@ -2308,7 +2300,6 @@
|
||||
<string name="simplex_address_or_1_time_link">Endereço SimpleX ou link único?</string>
|
||||
<string name="icon_descr_sound_muted">Som silenciado</string>
|
||||
<string name="v6_1_better_calls_descr">Alterne entre áudio e vídeo durante a chamada.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Selecione os operadores de rede que deseja utilizar.</string>
|
||||
<string name="operator_review_conditions">Revisar condições</string>
|
||||
<string name="simplex_address_and_1_time_links_are_safe_to_share">Endereços SimpleX e links únicos são seguros para compartilhar por meio de qualquer mensageiro.</string>
|
||||
<string name="onboarding_choose_server_operators">Operadores do servidor</string>
|
||||
@@ -2317,7 +2308,6 @@
|
||||
<string name="report_compose_reason_header_spam">Denunciar spam: somente os moderadores do grupo poderão ver.</string>
|
||||
<string name="error_server_protocol_changed">Protocolo do servidor alterado.</string>
|
||||
<string name="share_simplex_address_on_social_media">Compartilhe o endereço do SimpleX nas redes sociais.</string>
|
||||
<string name="onboarding_network_operators_review_later">Revisar depois</string>
|
||||
<string name="network_session_mode_server">Servidor</string>
|
||||
<string name="onboarding_network_operators_simplex_flux_agreement">O SimpleX Chat e o Flux fizeram um acordo para incluir servidores operados pelo Flux no aplicativo.</string>
|
||||
<string name="display_name_requested_to_connect">conexão solicitada</string>
|
||||
@@ -2330,13 +2320,10 @@
|
||||
<string name="v6_2_network_decentralization_descr">O segundo operador predefinido no aplicativo!</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Esta mensagem foi apagada ou ainda não foi recebida.</string>
|
||||
<string name="view_updated_conditions">Ver condições atualizadas</string>
|
||||
<string name="address_creation_instruction">Toque em Criar endereço SimpleX no menu para criá-lo mais tarde.</string>
|
||||
<string name="network_smp_web_port_footer">Usar porta TCP %1$s quando nenhuma porta for especificada.</string>
|
||||
<string name="report_archive_alert_desc">A denúncia será arquivada para você.</string>
|
||||
<string name="operator_use_for_messages_receiving">Para receber</string>
|
||||
<string name="change_automatic_chat_deletion_message">Esta ação não pode ser desfeita - as mensagens enviadas e recebidas nesta conversa antes do período selecionado serão apagadas.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Para se proteger contra a substituição do seu link, você pode comparar os códigos de segurança com os dos seus contatos.</string>
|
||||
<string name="unread_mentions">Menções não lidas</string>
|
||||
<string name="network_smp_web_port_section_title">Porta TCP para mensagens</string>
|
||||
<string name="network_smp_web_port_toggle">Usar porta web</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">O aplicativo protege sua privacidade usando diferentes operadores em cada conversa.</string>
|
||||
@@ -2349,12 +2336,9 @@
|
||||
<string name="onboarding_network_operators_configure_via_settings">É possível ajustar os servidores através das configurações.</string>
|
||||
<string name="use_servers_of_operator_x">Usar %s</string>
|
||||
<string name="operator_use_operator_toggle_description">Usar servidores</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="max_group_mentions_per_message_reached">Você pode mencionar até %1$s membros por mensagem!</string>
|
||||
<string name="settings_message_shape_tail">Final</string>
|
||||
<string name="maximum_message_size_reached_forwarding">Você pode copiar e reduzir o tamanho da mensagem para enviá-la.</string>
|
||||
<string name="onboarding_network_operators_update">Atualização</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Você pode configurar operadores em Configurações de rede & servidores.</string>
|
||||
<string name="operator_use_for_files">Usar para arquivos</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Você deixará de receber mensagens deste chat. O histórico do chat será preservado.</string>
|
||||
<string name="call_desktop_permission_denied_title">Para realizar chamadas, permita o uso do microfone. Encerre a chamada atual e tente novamente.</string>
|
||||
@@ -2395,13 +2379,11 @@
|
||||
<string name="channel_subscriber_count_plural">%1$d inscritos</string>
|
||||
<string name="badge_supported_simplex">%1$s apoiou o SimpleX Chat. O selo expirou em %2$s.</string>
|
||||
<string name="badge_support_from_v7">Você pode apoiar o SimpleX a partir da versão 7 do aplicativo.</string>
|
||||
<string name="settings_section_title_about">Sobre</string>
|
||||
<string name="accept_contact_request">Aceitar solicitação de contato</string>
|
||||
<string name="chat_banner_accept_contact_request">Aceitar solicitação de contato</string>
|
||||
<string name="relay_status_accepted">aceito</string>
|
||||
<string name="relay_status_acknowledged_roster">lista reconhecida</string>
|
||||
<string name="relay_status_active">ativo</string>
|
||||
<string name="add_button">Adicionar</string>
|
||||
<string name="v7_0_channels_contributors">Adicionar colaboradores.</string>
|
||||
<string name="add_description">Adicionar descrição</string>
|
||||
<string name="compose_view_add_message">Adicionar mensagem</string>
|
||||
@@ -2483,7 +2465,6 @@
|
||||
<string name="channel_full_name_field">Nome completo do canal:</string>
|
||||
<string name="channel_no_active_relays_try_later">O canal não possui relays ativos. Tente entrar novamente mais tarde.</string>
|
||||
<string name="chat_link_channel">Link do canal</string>
|
||||
<string name="button_channel_members">Membros do canal</string>
|
||||
<string name="channel_display_name_field">Nome do canal</string>
|
||||
<string name="channel_preferences">Preferências do canal</string>
|
||||
<string name="snd_channel_event_channel_profile_updated">perfil do canal atualizado</string>
|
||||
@@ -2517,8 +2498,6 @@
|
||||
<string name="relay_conn_status_connected">conectado</string>
|
||||
<string name="v6_4_connect_faster">Conecte-se mais rápido! 🚀</string>
|
||||
<string name="relay_conn_status_connecting">conectando</string>
|
||||
<string name="channel_name_requires_newer_app_version">Para se conectar usando o nome do canal, é necessário estar na versão mais recente do aplicativo.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Para se conectar usando o nome do contato, é necessário estar na versão mais recente do aplicativo.</string>
|
||||
<string name="info_row_connection_failed">A conexão falhou</string>
|
||||
<string name="connect_plan_connect_to_name">Conectar-se a %s</string>
|
||||
<string name="settings_section_title_contact">Contato</string>
|
||||
@@ -2582,7 +2561,6 @@
|
||||
<string name="error_deleting_member_support_chat">Erro ao apagar conversa</string>
|
||||
<string name="error_deleting_message">Erro ao apagar mensagem</string>
|
||||
<string name="error_marking_member_support_chat_read">Erro ao marcar como lida</string>
|
||||
<string name="error_opening_channel">Erro ao abrir canal</string>
|
||||
<string name="error_preparing_contact">Erro ao abrir conversa</string>
|
||||
<string name="error_preparing_group">Erro ao abrir grupo</string>
|
||||
<string name="error_rejecting_contact_request">Erro ao recusar solicitação de contato</string>
|
||||
@@ -2598,7 +2576,6 @@
|
||||
<string name="files_prohibited_in_this_chat">Arquivos e mídias não são permitidos neste chat.</string>
|
||||
<string name="info_row_file_servers">Servidores de arquivos</string>
|
||||
<string name="share_text_file_servers">Servidores de arquivos: %s</string>
|
||||
<string name="content_filter_menu_item">Filtro</string>
|
||||
<string name="proxy_destination_error_unknown_ca">A impressão digital no endereço do servidor de destino não corresponde ao certificado: %1$s.</string>
|
||||
<string name="smp_proxy_error_unknown_ca">A impressão digital do endereço do servidor de encaminhamento não corresponde à do certificado: %1$s.</string>
|
||||
<string name="network_error_unknown_ca">A impressão digital do endereço do servidor não corresponde à do certificado: %1$s.</string>
|
||||
@@ -2696,7 +2673,6 @@
|
||||
<string name="privacy_chat_list_open_clean_web_link">Abrir link limpo</string>
|
||||
<string name="open_external_link_title">Abrir link externo?</string>
|
||||
<string name="privacy_chat_list_open_full_web_link">Abrir link completo</string>
|
||||
<string name="connect_plan_open_new_channel">Abrir novo canal</string>
|
||||
<string name="connect_plan_open_new_chat">Abrir nova conversa</string>
|
||||
<string name="connect_plan_open_new_group">Abrir novo grupo</string>
|
||||
<string name="open_to_accept">Abrir para aceitar</string>
|
||||
@@ -2708,7 +2684,6 @@
|
||||
<string name="channel_members_section_owners">Proprietários e colaboradores</string>
|
||||
<string name="v6_5_ownership">Autonomia: você pode executar seus próprios relays.</string>
|
||||
<string name="group_member_status_pending_review">aguardando revisão</string>
|
||||
<string name="please_upgrade_the_app">Atualize o aplicativo.</string>
|
||||
<string name="snd_group_event_user_pending_review">Aguarde os moderadores do grupo analisarem seu pedido para participar.</string>
|
||||
<string name="preset_relay_address">Endereço do relay predefinido</string>
|
||||
<string name="preset_relay_name">Nome do relay predefinido</string>
|
||||
@@ -2780,7 +2755,6 @@
|
||||
<string name="compose_view_send_request">Enviar solicitação</string>
|
||||
<string name="compose_view_send_request_without_message">Enviar solicitação sem mensagem</string>
|
||||
<string name="onboarding_send_1_time_link">Envie o link por qualquer aplicativo de mensagens - é seguro. Peça para colar no SimpleX.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Será enviado ao seu contato após a conexão.</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">O servidor %1$s não suporta resolução de nomes. Configure os servidores ou use um link de conexão.</string>
|
||||
<string name="error_relay_test_server_auth">O servidor requer autorização para se conectar ao relay; verifique a senha.</string>
|
||||
<string name="server_warning">Aviso do servidor</string>
|
||||
@@ -2805,7 +2779,6 @@
|
||||
<string name="sign_messages">Assinar mensagens</string>
|
||||
<string name="tray_tooltip">SimpleX</string>
|
||||
<string name="simplex_link_channel">Link do canal SimpleX</string>
|
||||
<string name="simplex_name">Nome SimpleX</string>
|
||||
<string name="simplex_name_error">Erro no nome SimpleX</string>
|
||||
<string name="simplex_name_not_verified">Nome SimpleX não verificado</string>
|
||||
<string name="v7_0_simplex_names">Nomes públicos SimpleX (BETA)</string>
|
||||
@@ -2843,8 +2816,6 @@
|
||||
<string name="content_filter_videos">Vídeos</string>
|
||||
<string name="voice_recording_not_supported">Seu dispositivo não tem suporte para gravação de áudio</string>
|
||||
<string name="unsupported_connection_link">Link de conexão não suportado</string>
|
||||
<string name="unsupported_channel_name">Nome de canal não suportado</string>
|
||||
<string name="unsupported_contact_name">Nome de contato não suportado</string>
|
||||
<string name="simplex_name_unconfirmed">Nome não confirmado</string>
|
||||
<string name="your_public_address">Seu endereço público</string>
|
||||
<string name="simplex_name_unconfirmed_desc">O nome SimpleX %1$s está registrado, mas não está associado ao perfil. Se você for o proprietário, adicione-o ao perfil do endereço ou canal.</string>
|
||||
@@ -2883,7 +2854,6 @@
|
||||
<string name="why_built_heading">Você nasceu sem uma conta.</string>
|
||||
<string name="onboarding_your_network">Sua rede</string>
|
||||
<string name="alert_text_msg_reception_error">O aplicativo removeu esta mensagem após %1$d tentativas de recebê-la.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Esta configuração se aplica ao seu perfil atual</string>
|
||||
<string name="rcv_channel_event_updated_channel_profile">perfil do canal atualizado</string>
|
||||
<string name="snd_group_event_member_accepted">você aceitou este membro</string>
|
||||
<string name="webpage_code">Código da página da web</string>
|
||||
|
||||
@@ -250,7 +250,6 @@
|
||||
<string name="color_sent_quote">Răspuns trimis</string>
|
||||
<string name="share_image">Partajează media…</string>
|
||||
<string name="share_message">Partajează mesaj…</string>
|
||||
<string name="chat_list_always_visible">Afișează lista de conversații într-o fereastră nouă</string>
|
||||
<string name="terminal_always_visible">Afișează consola în fereastră nouă</string>
|
||||
<string name="setup_database_passphrase">Configurează parola bazei de date</string>
|
||||
<string name="set_database_passphrase">Setează parola bazei de date</string>
|
||||
@@ -677,7 +676,6 @@
|
||||
<string name="xftp_servers_configured">Servere XFTP configurate</string>
|
||||
<string name="set_password_to_export_desc">Baza de date este criptată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare.</string>
|
||||
<string name="info_view_call_button">apel</string>
|
||||
<string name="info_view_connect_button">conectare</string>
|
||||
<string name="contact_deleted">Contact șters!</string>
|
||||
<string name="conversation_deleted">Conversație ștearsă!</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Actualizarea aplicației este descărcată</string>
|
||||
@@ -914,7 +912,6 @@
|
||||
<string name="no_unread_chats">Nicio conversație necitită</string>
|
||||
<string name="no_chats_in_list">Nicio conversație în lista %s.</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nimic selectat</string>
|
||||
<string name="info_view_open_button">deschis</string>
|
||||
<string name="settings_section_title_help">Ajutor</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Doar contactul tău poate trimite mesaje care dispar.</string>
|
||||
<string name="migrate_to_device_importing_archive">Se importă arhiva</string>
|
||||
@@ -1035,7 +1032,6 @@
|
||||
<string name="no_filtered_chats">Nicio conversație filtrată</string>
|
||||
<string name="smp_servers_new_server">Server nou</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Asigurați -vă că adresele Serverului WebRTC ICE sunt în format corect, separate pe linii și nu sunt duplicate.</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Rutarea mesajelor</string>
|
||||
<string name="edit_image">Editează imaginea</string>
|
||||
<string name="permissions_open_settings">Deschide setările</string>
|
||||
<string name="permissions_grant_in_settings">Acordare în setări</string>
|
||||
@@ -1217,7 +1213,6 @@
|
||||
<string name="v5_5_new_interface_languages">Interfață de utilizator maghiară și turcă</string>
|
||||
<string name="v5_7_forward">Redirecționează și salvează mesajele</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Activează Flux în setările Rețea și servere pentru o mai bună confidențialitate a metadatelor.</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">pentru o mai bună confidențialitate a metadatelor.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Redirecționați până la 20 de mesaje simultan.</string>
|
||||
<string name="v6_3_organize_chat_lists_descr">Nu rata mesajele importante.</string>
|
||||
<string name="v6_3_organize_chat_lists">Organizează conversațiile în liste</string>
|
||||
@@ -1587,7 +1582,6 @@
|
||||
<string name="errors_in_servers_configuration">Erori în configurarea serverelor.</string>
|
||||
<string name="smp_proxy_error_broker_host">Adresa serverului de redirecționare este incompatibilă cu setările rețelei: %1$s.</string>
|
||||
<string name="no_media_servers_configured_for_sending">Nu există servere pentru a trimite fișiere.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mod de rutare a mesajelor</string>
|
||||
<string name="network_smp_proxy_mode_never">Niciodată</string>
|
||||
<string name="icon_descr_flip_camera">Întoarce camera</string>
|
||||
<string name="invalid_file_link">Link nevalid</string>
|
||||
@@ -1751,7 +1745,6 @@
|
||||
<string name="report_verb">Raport</string>
|
||||
<string name="switch_receiving_address_desc">Adresa de primire va fi schimbată la un alt server. Schimbarea adresei se va finaliza după ce expeditorul se conectează online.</string>
|
||||
<string name="save_admission_question">Salvezi setările de acces?</string>
|
||||
<string name="onboarding_network_operators_review_later">Revizuiți mai târziu</string>
|
||||
<string name="receipts_groups_override_disabled">Trimiterea confirmărilor este dezactivată pentru %d grupuri</string>
|
||||
<string name="group_member_status_pending_review_short">recenzie</string>
|
||||
<string name="error_server_operator_changed">Operatorul serverului s-a schimbat.</string>
|
||||
@@ -1803,8 +1796,6 @@
|
||||
<string name="settings_section_title_delivery_receipts">Trimite confirmări de livrare la</string>
|
||||
<string name="self_destruct_passcode_changed">Parola de autodistrugere a fost schimbată!</string>
|
||||
<string name="info_row_updated_at">Înregistrare actualizată la</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Selectează operatorii de rețea de utilizat.</string>
|
||||
<string name="remote_hosts_section">Telefoane mobile la distanță</string>
|
||||
<string name="send_receipts">Trimite confirmări de citire</string>
|
||||
<string name="accept_feature_set_1_day">Setați 1 zi</string>
|
||||
<string name="v5_1_self_destruct_passcode">Parolă de autodistrugere</string>
|
||||
@@ -1965,8 +1956,6 @@
|
||||
<string name="tap_to_paste_link">Atinge pentru a lipi linkul</string>
|
||||
<string name="the_text_you_pasted_is_not_a_link">Textul pe care l-ai lipit nu este un link SimpleX.</string>
|
||||
<string name="app_check_for_updates_stable">Stabil</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Puteți configura operatorii în setările Rețea și servere.</string>
|
||||
<string name="onboarding_network_operators_update">Actualizare</string>
|
||||
<string name="video_call_no_encryption">apel video (necriptat e2e)</string>
|
||||
<string name="alert_text_msg_bad_id">ID-ul următorului mesaj este incorect (mai mic sau egal cu precedentul).\nSe poate întâmpla din cauza unei erori sau când conexiunea este compromisă.</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru serverele de fișiere.</string>
|
||||
@@ -2016,7 +2005,6 @@
|
||||
<string name="member_role_will_be_changed_with_notification_chat">Rolul va fi schimbat în %s. Toți participanții la chat vor fi notificați.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Rolul va fi schimbat în %s. Toți membrii grupului vor fi notificați.</string>
|
||||
<string name="use_servers_of_operator_x">Folosește %s</string>
|
||||
<string name="operator_website">Site web</string>
|
||||
<string name="operator_servers_title">%s Servere</string>
|
||||
<string name="update_network_settings_question">Actualizați setările de rețea?</string>
|
||||
<string name="tap_to_activate_profile">Atingeți pentru a activa profilul.</string>
|
||||
@@ -2155,8 +2143,6 @@
|
||||
<string name="lock_not_enabled">Blocarea SimpleX nu este activă!</string>
|
||||
<string name="la_lock_mode_system">Autentificare sistem</string>
|
||||
<string name="database_initialization_error_desc">Baza de date nu funcționează corect. Atingeți pentru a afla mai multe.</string>
|
||||
<string name="report_archive_alert_desc">Raportul va fi arhivat.</string>
|
||||
<string name="address_creation_instruction">Atingeți Creează o adresă SimpleX în meniu pentru a o crea mai târziu.</string>
|
||||
<string name="this_text_is_available_in_settings">Acest text este disponibil în setări</string>
|
||||
<string name="group_preview_you_are_invited">Ești invitat în grup</string>
|
||||
<string name="icon_descr_waiting_for_image">Se așteaptă imaginea</string>
|
||||
@@ -2164,7 +2150,6 @@
|
||||
<string name="icon_descr_waiting_for_video">Se așteaptă videoclipul</string>
|
||||
<string name="temporary_file_error">Eroare temporară de fișier</string>
|
||||
<string name="unfavorite_chat">Elimină din favorite</string>
|
||||
<string name="unread_mentions">Mențiuni necitite</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Conexiunea pe care ați acceptat-o va fi anulată!</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Contactul cu care ați partajat acest link NU se va putea conecta!</string>
|
||||
<string name="contact_wants_to_connect_with_you">vrea să se conecteze cu tine!</string>
|
||||
@@ -2286,7 +2271,6 @@
|
||||
<string name="non_content_uri_alert_text">Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 mobil: atinge <b>Deschide în aplicația mobilă</b>, apoi atinge <b>Conectare</b> în aplicație.]]></string>
|
||||
<string name="add_short_link">Actualizează adresa</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Condițiile vor fi acceptate pentru operatorii activați după 30 de zile.</string>
|
||||
<string name="call_desktop_permission_denied_chrome">Apasă pe butonul de informații de lângă bara de adrese pentru a permite accesul la microfon.</string>
|
||||
<string name="settings_message_shape_corner">Colţ</string>
|
||||
<string name="chat_database_exported_save">Puteți salva arhiva exportată.</string>
|
||||
@@ -2432,11 +2416,9 @@
|
||||
<string name="v6_4_connect_faster_descr">Te poți conecta la conversație și poți trimite mesaje imediat ce apeși pe Conectare.</string>
|
||||
<string name="v6_4_role_moderator">Rol nou în grup: Moderator</string>
|
||||
<string name="group_short_descr_field">Descriere scurtă:</string>
|
||||
<string name="sent_to_your_contact_after_connection">Trimis contactului tău după conectare.</string>
|
||||
<string name="share_profile_via_link">Actualizezi la o adresă permanentă?</string>
|
||||
<string name="address_welcome_message">Mesaj de bun venit</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Solicitări de contact de la grupuri</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Această setare este pentru profilul tău actual</string>
|
||||
<string name="share_old_address_alert_button">Partajează adresa veche</string>
|
||||
<string name="share_old_link_alert_button">Partajează linkul vechi</string>
|
||||
<string name="loading_profile">Se încarcă profilul…</string>
|
||||
@@ -2516,7 +2498,6 @@
|
||||
<string name="channel_subscriber_count_singular">%1$d abonat</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d abonați</string>
|
||||
<string name="badge_supported_simplex">%1$s a susținut SimpleX Chat. Insigna a expirat pe %2$s.</string>
|
||||
<string name="settings_section_title_about">Despre</string>
|
||||
<string name="relay_status_accepted">acceptată</string>
|
||||
<string name="app_update_required">Aplicația necesita actualizare</string>
|
||||
<string name="badge_unknown_key_title">Eticheta nu poate fi verificată</string>
|
||||
|
||||
@@ -1328,7 +1328,6 @@
|
||||
<string name="no_history">Нет истории</string>
|
||||
<string name="receipts_contacts_override_enabled">Отправка отчётов о доставке включена для %d контактов</string>
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Установка для Вашего активного профиля</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Установки для Вашего активного профиля</string>
|
||||
<string name="receipts_contacts_override_disabled">Отправка отчётов о доставке выключена для %d контактов</string>
|
||||
<string name="sync_connection_force_desc">Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!</string>
|
||||
@@ -1853,7 +1852,6 @@
|
||||
<string name="message_queue_info_server_info">информация сервера об очереди: %1$s
|
||||
\n
|
||||
\nпоследнее полученное сообщение: %2$s</string>
|
||||
<string name="chat_list_always_visible">Показать список чатов в новом окне</string>
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов или когда SOCKS-прокси включен).</string>
|
||||
<string name="network_smp_proxy_mode_unprotected">Незащищённый</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Без Tor или VPN, Ваш IP-адрес будет доступен серверам файлов.</string>
|
||||
@@ -1895,8 +1893,6 @@
|
||||
<string name="network_smp_proxy_mode_unknown">Неизвестные серверы</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Нет</string>
|
||||
<string name="private_routing_show_message_status">Показать статус сообщения</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Прямая доставка сообщений</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Режим доставки сообщений</string>
|
||||
<string name="settings_section_title_private_message_routing">Конфиденциальная доставка сообщений</string>
|
||||
<string name="settings_section_title_chat_colors">Цвета чата</string>
|
||||
<string name="settings_section_title_chat_theme">Тема чата</string>
|
||||
@@ -1934,7 +1930,6 @@
|
||||
<string name="cant_call_contact_alert_title">Не удаётся позвонить контакту</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Не удаётся написать члену группы</string>
|
||||
<string name="app_check_for_updates">Проверять обновления</string>
|
||||
<string name="info_view_connect_button">соединиться</string>
|
||||
<string name="proxy_destination_error_broker_host">Адрес сервера назначения %1$s несовместим с настройками пересылающего сервера %2$s.</string>
|
||||
<string name="smp_proxy_error_connecting">Ошибка подключения к пересылающему серверу %1$s. Попробуйте позже.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Пересылающий сервер %1$s не смог подключиться к серверу назначения %2$s. Попробуйте позже.</string>
|
||||
@@ -2045,7 +2040,6 @@
|
||||
<string name="member_inactive_title">Член группы неактивен</string>
|
||||
<string name="message_forwarded_desc">Прямого соединения пока нет, сообщение переслано или будет переслано админом.</string>
|
||||
<string name="selected_chat_items_nothing_selected">Ничего не выбрано</string>
|
||||
<string name="info_view_open_button">открыть</string>
|
||||
<string name="info_view_search_button">поиск</string>
|
||||
<string name="selected_chat_items_selected_n">Выбрано %d</string>
|
||||
<string name="toolbar_settings">Настройки</string>
|
||||
@@ -2197,7 +2191,6 @@
|
||||
<string name="no_media_servers_configured_for_private_routing">Нет серверов для приёма файлов.</string>
|
||||
<string name="no_media_servers_configured_for_sending">Нет серверов для отправки файлов.</string>
|
||||
<string name="connection_error_quota">Недоставленные сообщения</string>
|
||||
<string name="address_creation_instruction">Нажмите Создать адрес SimpleX в меню, чтобы создать его позже.</string>
|
||||
<string name="address_or_1_time_link">Адрес или одноразовая ссылка?</string>
|
||||
<string name="connection_security">Безопасность соединения</string>
|
||||
<string name="onboarding_choose_server_operators">Операторы серверов</string>
|
||||
@@ -2207,7 +2200,6 @@
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[Условия приняты для оператора(ов): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Условия будут автоматически приняты для включенных операторов: %s.</string>
|
||||
<string name="operator_conditions_accepted_on">Условия приняты: %s</string>
|
||||
<string name="operator_website">Вебсайт</string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Условия уже приняты для следующих операторов: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_applied"><![CDATA[Эти условия также будут применены к: <b>%s</b>.]]></string>
|
||||
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[Чтобы использовать серверы оператора <b>%s</b>, примите условия использования.]]></string>
|
||||
@@ -2223,7 +2215,6 @@
|
||||
<string name="v6_2_network_decentralization">Децентрализация сети</string>
|
||||
<string name="v6_2_network_decentralization_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="view_updated_conditions">Посмотреть измененные условия</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Устройства Xiaomi</b>: пожалуйста, включите опцию Autostart в системных настройках для работы нотификаций.]]></string>
|
||||
@@ -2253,12 +2244,7 @@
|
||||
<string name="onboarding_notifications_mode_battery">Нотификации и батарейка</string>
|
||||
<string name="how_it_helps_privacy">Как это улучшает конфиденциальность</string>
|
||||
<string name="onboarding_network_operators">Операторы серверов</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_continue">Продолжить</string>
|
||||
<string name="onboarding_network_operators_review_later">Посмотреть позже</string>
|
||||
<string name="onboarding_network_operators_update">Обновить</string>
|
||||
<string name="remote_hosts_section">Связанные мобильные устройства</string>
|
||||
<string name="leave_chat_question">Покинуть разговор?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Вы прекратите получать сообщения в этом разговоре. История будет сохранена.</string>
|
||||
<string name="button_add_friends">Добавить друзей</string>
|
||||
@@ -2313,7 +2299,6 @@
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Приложение улучшает конфиденциальность, используя разных операторов в каждом разговоре.</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем.</string>
|
||||
<string name="failed_to_save_servers">Ошибка сохранения серверов</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Условия будут приняты для включенных операторов через 30 дней.</string>
|
||||
<string name="error_accepting_operator_conditions">Ошибка приёма условий</string>
|
||||
<string name="connection_error_quota_desc">Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети.</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности.</string>
|
||||
@@ -2368,7 +2353,6 @@
|
||||
<string name="v6_3_private_media_file_names">Конфиденциальные названия медиафайлов.</string>
|
||||
<string name="blocking_reason_spam">Спам</string>
|
||||
<string name="chat_list_group_reports">Сообщения о нарушениях</string>
|
||||
<string name="unread_mentions">Непрочитанные упоминания</string>
|
||||
<string name="privacy_chat_list_open_links_yes">Да</string>
|
||||
<string name="v6_3_mentions">Упоминайте членов группы 👋</string>
|
||||
<string name="v6_3_better_privacy_and_security">Улучшенная конфиденциальность и безопасность</string>
|
||||
@@ -2380,7 +2364,6 @@
|
||||
<string name="v6_3_set_message_expiration_in_chats">Установите срок хранения сообщений в чатах.</string>
|
||||
<string name="max_group_mentions_per_message_reached">Вы можете упомянуть до %1$s пользователей в одном сообщении!</string>
|
||||
<string name="report_reason_alert_title">Причина сообщения?</string>
|
||||
<string name="report_archive_alert_desc">Эта жалоба будет архивирована для вас.</string>
|
||||
<string name="enable_sending_member_reports">Разрешить отправлять сообщения о нарушениях модераторам.</string>
|
||||
<string name="blocking_reason_content">Содержание нарушает условия использования</string>
|
||||
<string name="error_reading_passphrase">Ошибка чтения пароля базы данных</string>
|
||||
@@ -2549,7 +2532,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="share_group_profile_via_link">Обновить ссылку группы?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Обновить</string>
|
||||
<string name="share_profile_via_link">Обновить адрес?</string>
|
||||
@@ -2617,7 +2599,6 @@
|
||||
<string name="remove_member_delete_messages_confirmation">Удалить вместе с сообщениями</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>
|
||||
@@ -2636,7 +2617,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="snd_channel_event_channel_profile_updated">профиль канала обновлён</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">Канал будет удалён для всех подписчиков - это нельзя отменить!</string>
|
||||
@@ -2656,7 +2636,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="invalid_relay_address">Неверный адрес релея!</string>
|
||||
@@ -2715,7 +2694,6 @@
|
||||
<string name="relay_status_inactive">неактивен</string>
|
||||
<string name="owner_verification_passed">Подпись ссылки проверена.</string>
|
||||
<string name="connect_plan_open_channel">Открыть канал</string>
|
||||
<string name="connect_plan_open_new_channel">Открыть новый канал</string>
|
||||
<string name="channel_members_section_owners">Владельцы</string>
|
||||
<string name="member_info_section_title_owner">Владелец</string>
|
||||
<string name="group_member_role_relay">релей</string>
|
||||
@@ -2878,7 +2856,6 @@
|
||||
<string name="one_hand_ui_bottom_bar">Нижнее меню</string>
|
||||
<string name="link_previews_alert_desc_socks">Картинка ссылки будет загружена через SOCKS-прокси. DNS-запрос может быть локальным через Ваш резолвер.</string>
|
||||
<string name="one_hand_ui_top_bar">Верхнее меню</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>
|
||||
@@ -2887,7 +2864,6 @@
|
||||
<string name="channel_owner_count_plural">%1$d владельцев</string>
|
||||
<string name="channel_owners_contributors_count">%1$d владельцев и участников канала</string>
|
||||
<string name="badge_supported_simplex">%1$s поддерживал SimpleX Chat. Срок действия бейджа истек %2$s.</string>
|
||||
<string name="settings_section_title_about">О приложении</string>
|
||||
<string name="advanced_settings">Продвинутые настройки</string>
|
||||
<string name="another_instance_title">Приложение уже запущено</string>
|
||||
<string name="app_update_required">Необходимо обновление приложения</string>
|
||||
@@ -2909,8 +2885,6 @@
|
||||
<string name="channel_webpage">Веб-страница канала</string>
|
||||
<string name="num_relays_selected">Выбрано %d релеев</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="group_member_role_observer_channel">подписчик</string>
|
||||
@@ -2949,9 +2923,6 @@
|
||||
<string name="close_behavior_dialog_text">Если Вы выберете Закрыть, сообщения не будут доставляться.\nВы можете изменить это позже в настройках Интерфейса.</string>
|
||||
<string name="webpage_url_footer">Адрес будет показан подписчикам и разрешит загрузку предпросмотра.</string>
|
||||
<string name="connect_plan_join_name">Вступить в канал %s</string>
|
||||
<string name="unsupported_channel_name">Неподдерживаемое имя канала</string>
|
||||
<string name="unsupported_contact_name">Неподдерживаемое имя контакта</string>
|
||||
<string name="please_upgrade_the_app">Пожалуйста, обновите приложение.</string>
|
||||
<string name="simplex_name_error">Ошибка SimpleX имени</string>
|
||||
<string name="simplex_name_no_servers_desc">Ни один из Ваших серверов не настроен для разрешения SimpleX имён. Настройте серверы или используйте ссылку для соединения.</string>
|
||||
<string name="simplex_name_server_no_resolver_desc">Сервер %1$s не поддерживает разрешение имён. Настройте серверы или используйте ссылку для соединения.</string>
|
||||
@@ -2980,7 +2951,6 @@
|
||||
<string name="verify_simplex_name_action">Проверить имя</string>
|
||||
<string name="verify_simplex_names">Проверять SimpleX имена</string>
|
||||
<string name="simplex_name_not_verified">SimpleX имя не проверено</string>
|
||||
<string name="simplex_name">SimpleX имя</string>
|
||||
<string name="your_simplex_name">Ваше SimpleX имя</string>
|
||||
<string name="set_simplex_name">Установить SimpleX имя</string>
|
||||
<string name="simplex_name_owner_no_channel_link">SimpleX имя %1$s зарегистрировано без ссылки канала. Добавьте ссылку канала к имени на странице регистрации.</string>
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
<string name="abort_switch_receiving_address_confirm">Prerušiť</string>
|
||||
<string name="abort_switch_receiving_address">Prerušiť zmenu adresy</string>
|
||||
<string name="abort_switch_receiving_address_question">Prerušiť zmenu adresy?</string>
|
||||
<string name="settings_section_title_about">O aplikácii</string>
|
||||
<string name="onboarding_network_about_operators">O operátoroch</string>
|
||||
<string name="about_simplex">O SimpleX</string>
|
||||
<string name="learn_more_about_address">O adrese SimpleX</string>
|
||||
@@ -82,7 +81,6 @@
|
||||
<string name="network_enable_socks_info">Pristupovať k serverom cez SOCKS proxy na porte %d? Pred povolením tejto možnosti musí byť spustená proxy.</string>
|
||||
<string name="relay_status_active">aktívne</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">aktívne spojenia</string>
|
||||
<string name="add_button">Pridať</string>
|
||||
<string name="add_contact_tab">Pridať kontakt</string>
|
||||
<string name="v7_0_channels_contributors">Pridať prispievateľov.</string>
|
||||
<string name="add_description">Pridať opis</string>
|
||||
@@ -367,7 +365,6 @@
|
||||
<string name="channel_no_active_relays_try_later">Kanál nemá žiadne aktívne relé. Prosím, skúste sa pripojiť neskôr.</string>
|
||||
<string name="chat_link_channel">Odkaz kanálu</string>
|
||||
<string name="channel_link">Odkaz kanálu</string>
|
||||
<string name="button_channel_members">Členovia kanálu</string>
|
||||
<string name="channel_display_name_field">Názov kanálu</string>
|
||||
<string name="channel_preferences">Vlastnosti kanálu</string>
|
||||
<string name="chat_list_channels">Kanály</string>
|
||||
@@ -753,7 +750,6 @@
|
||||
<string name="icon_descr_close_button">Tlačidlo zavrieť</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Porovnajte bezpečnostné kódy s vašimi kontaktmi.</string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Podmienky už boli prijaté pre tohto/týchto operátora(ov): <b>%s</b>.]]></string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Podmienky budú prijaté pre povolených operátorov po 30 dňoch.</string>
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Podmienky budú prijaté pre operátor(y): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Podmienky budú prijaté pre operátora(y): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Podmienky budú prijaté dňa: %s.</string>
|
||||
@@ -1124,7 +1120,6 @@
|
||||
<string name="files_and_media_section">Súbory a médiá</string>
|
||||
<string name="share_text_file_status">Status súboru: %s</string>
|
||||
<string name="revoke_file__message">Súbor bude vymazaný zo serverov.</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="sync_connection_confirm">Opraviť</string>
|
||||
<string name="fix_connection_confirm">Opraviť</string>
|
||||
<string name="fix_connection">Opraviť pripojenie</string>
|
||||
@@ -1303,7 +1298,6 @@
|
||||
<string name="error_loading_xftp_servers">Chyba pri načítavaní XFTP serverov</string>
|
||||
<string name="error_marking_member_support_chat_read">Chyba pri označovaní ako prečítané</string>
|
||||
<string name="unable_to_open_browser_title">Chyba pri otváraní prehliadača</string>
|
||||
<string name="error_opening_channel">Chyba pri otváraní kanálu</string>
|
||||
<string name="error_preparing_contact">Chyba pri otváraní chatu</string>
|
||||
<string name="error_preparing_group">Chyba pri otváraní skupiny</string>
|
||||
<string name="error_rejecting_contact_request">Chyba pri odmietaní žiadosti o kontakt</string>
|
||||
@@ -1388,7 +1382,6 @@
|
||||
<string name="chat_link_one_time">Jednorazový odkaz</string>
|
||||
<string name="images_limit_desc">Naraz sa dá poslať iba 10 obrázkov</string>
|
||||
<string name="videos_limit_desc">Naraz sa dá poslať iba 10 videí</string>
|
||||
<string name="info_view_open_button">otvoriť</string>
|
||||
<string name="open_verb">Otvoriť</string>
|
||||
<string name="rcv_group_event_open_chat">Otvoriť</string>
|
||||
<string name="turn_off_system_restriction_button">Otvoriť nastavenia aplikácie</string>
|
||||
@@ -1400,7 +1393,6 @@
|
||||
<string name="open_database_folder">Otvoriť priečinok databázy</string>
|
||||
<string name="open_external_link_title">Otvoriť externý odkaz?</string>
|
||||
<string name="privacy_chat_list_open_full_web_link">Otvoriť celý odkaz</string>
|
||||
<string name="connect_plan_open_new_channel">Otvoriť nový kanál</string>
|
||||
<string name="open_server_settings_button">Otvoriť nastavenia serveru</string>
|
||||
<string name="permissions_open_settings">Otvoriť nastavenia</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Otvorte SimpleX Chat na prijatie hovoru</string>
|
||||
|
||||
@@ -1774,12 +1774,10 @@
|
||||
<string name="network_smp_proxy_mode_unknown">Bilinmeyen sunucular</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Her zaman gizli yönlendirmeyi kullan.</string>
|
||||
<string name="network_smp_proxy_mode_never_description">Gizli yönlendirmeyi KULLANMA.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Mesaj yönlendirme modu</string>
|
||||
<string name="network_smp_proxy_mode_never">Hiçbir zaman</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Bilinmeyen sunucularla gizli yönlendirme kullan.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_downgrade">Sürüm düşürmeye izin ver</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Hayır</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Mesaj yönlendirme yedeklemesi</string>
|
||||
<string name="network_smp_proxy_mode_always">Her zaman</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN.</string>
|
||||
<string name="network_smp_proxy_fallback_allow_protected_description">IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</string>
|
||||
@@ -1819,7 +1817,6 @@
|
||||
<string name="theme_remove_image">Resmi kaldır</string>
|
||||
<string name="wallpaper_scale_repeat">Tekrarla</string>
|
||||
<string name="reset_single_color">Rengi sıfırla</string>
|
||||
<string name="chat_list_always_visible">Sohbet listesini yeni pencerede göster</string>
|
||||
<string name="file_not_approved_title">Bilinmeyen sunucular!</string>
|
||||
<string name="file_not_approved_descr">Tor veya VPN olmadan, IP adresiniz bu XFTP yönlendiricileri tarafından görülebilir:
|
||||
\n%1$s.</string>
|
||||
@@ -1871,11 +1868,9 @@
|
||||
<string name="forward_multiple">Mesajları ilet..</string>
|
||||
<string name="compose_forward_messages_n">%1$s mesaj iletiliyor</string>
|
||||
<string name="compose_save_messages_n">%1$s Mesaj kayıt ediliyor.</string>
|
||||
<string name="info_view_connect_button">bağlan</string>
|
||||
<string name="info_view_call_button">Ara</string>
|
||||
<string name="info_view_message_button">mesaj</string>
|
||||
<string name="only_delete_conversation">Sadece sohbeti sil</string>
|
||||
<string name="info_view_open_button">açık</string>
|
||||
<string name="info_view_search_button">ara</string>
|
||||
<string name="switching_profile_error_title">Profil değiştirme sırasında hata oluştu.</string>
|
||||
<string name="select_chat_profile">Sohbet profili seç</string>
|
||||
@@ -2167,9 +2162,7 @@
|
||||
<string name="delete_chat_question">Sohbet silinsin mi?</string>
|
||||
<string name="connection_not_ready">Bağlantı hazır değil.</string>
|
||||
<string name="operator_conditions_of_use">Kullanım şartları</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">daha iyi üstveri gizliliği için.</string>
|
||||
<string name="failed_to_save_servers">Sunucuları kaydederken hata oluştu</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Şartlar 30 gün sonra etkin operatörler için kabul edilecektir.</string>
|
||||
<string name="delete_report">Raporu sil</string>
|
||||
<string name="change_list">Listeyi değiştir</string>
|
||||
<string name="operators_conditions_will_also_apply"><![CDATA[Bu koşullar aynı zamanda: <b>%s</b> için de geçerli olacaktır.]]></string>
|
||||
@@ -2315,7 +2308,6 @@
|
||||
<string name="operator_updated_conditions">Güncellenen koşullar</string>
|
||||
<string name="report_item_visibility_submitter">Sadece siz ve moderatörler görebilir.</string>
|
||||
<string name="report_item_visibility_moderators">Sadece gönderen ve moderatörler görebilir.</string>
|
||||
<string name="address_creation_instruction">Daha sonra oluşturmak için menüden SimpleX adresi oluştur seçeneğine dokunun.</string>
|
||||
<string name="operator">Operatör</string>
|
||||
<string name="operator_use_for_files">Dosyalar için kullanın</string>
|
||||
<string name="v6_3_set_message_expiration_in_chats">Sohbetlerde mesajların geçerlilik süresini ayarlayın.</string>
|
||||
@@ -2339,7 +2331,6 @@
|
||||
<string name="blocking_reason_spam">İstenmeyen</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Uygulama, her konuşmada farklı operatörler kullanarak gizliliğinizi korur.</string>
|
||||
<string name="member_role_will_be_changed_with_notification_chat">Rol %s olarak değiştirilecek. Sohbetteki herkes bilgilendirilecek.</string>
|
||||
<string name="unread_mentions">Okunmamış bahsetmeler</string>
|
||||
<string name="operator_review_conditions">Koşulları gözden geçirin</string>
|
||||
<string name="chat_main_profile_sent">Sohbet profiliniz sohbet üyelerine gönderilecektir.</string>
|
||||
<string name="your_servers">Sunucularınız</string>
|
||||
@@ -2347,7 +2338,6 @@
|
||||
<string name="change_automatic_chat_deletion_message">Bu işlem geri alınamaz - seçilen tarihten önce bu sohbette gönderilen ve alınan mesajlar silinecektir.</string>
|
||||
<string name="you_can_set_connection_name_to_remember">Bağlantının kimlerle paylaşıldığını hatırlamak için bağlantı adını ayarlayabilirsiniz.</string>
|
||||
<string name="share_simplex_address_on_social_media">SimpleX adresini sosyal medyada paylaşın.</string>
|
||||
<string name="operator_website">Web sitesi</string>
|
||||
<string name="unsupported_connection_link">Desteklenmeyen bağlantı bağlantısı</string>
|
||||
<string name="report_verb">Rapor</string>
|
||||
<string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX adresi ve tek kullanımlık bağlantılar herhangi bir mesajlaşma uygulaması üzerinden güvenle paylaşılabilir.</string>
|
||||
@@ -2355,9 +2345,6 @@
|
||||
<string name="save_admission_question">Giriş ayarlarını kaydetmek ister misiniz?</string>
|
||||
<string name="onboarding_conditions_private_chats_not_accessible">Operatörler şunları taahhüt eder:\n- Bağımsız olun\n- Meta veri kullanımını en aza indirin\n- Doğrulanmış açık kaynak kodunu çalıştırın</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Birden fazla operatör etkinleştirildiğinde, hiçbirinin kimin kiminle iletişim kurduğunu öğrenmek için meta verisi yoktur.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Kullanılacak ağ operatörlerini seçin.</string>
|
||||
<string name="onboarding_network_operators_update">Güncelleme</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Ağ ve sunucular ayarlarında operatörleri yapılandırabilirsiniz.</string>
|
||||
<string name="privacy_chat_list_open_web_link">Bağlantıyı aç</string>
|
||||
<string name="restore_passphrase_can_not_be_read_enter_manually_desc">Anahtar deposundaki parola okunamıyor, lütfen elle girin. Bu, uygulamayla uyumsuz bir sistem güncellemesinden sonra meydana gelmiş olabilir. Eğer durum bu değilse, lütfen geliştiricilerle iletişime geçin.</string>
|
||||
<string name="snd_group_event_member_accepted">Bu üyeyi kabul ettiniz.</string>
|
||||
@@ -2368,7 +2355,6 @@
|
||||
<string name="v6_2_network_decentralization_descr">Uygulamadaki ikinci önceden ayarlanmış operatör!</string>
|
||||
<string name="add_short_link">Adresi güncelle</string>
|
||||
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Gizlilik politikası ve kullanım koşulları.</string>
|
||||
<string name="onboarding_network_operators_review_later">Daha sonra incele</string>
|
||||
<string name="onboarding_choose_server_operators">Sunucu operatörleri</string>
|
||||
<string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat ve Flux, Flux tarafından işletilen sunucuların uygulamaya dahil edilmesi konusunda bir anlaşma yaptı.</string>
|
||||
<string name="privacy_chat_list_open_links">Sohbet listesinden bağlantıları aç</string>
|
||||
@@ -2409,7 +2395,6 @@
|
||||
<string name="report_sent_alert_title">Moderatörlere gönderilen rapor</string>
|
||||
<string name="report_sent_alert_msg_view_in_support_chat">Raporlarınızı yöneticilerle sohbet bölümünde görüntüleyebilirsiniz.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Ayarlar aracılığıyla sunucuları yapılandırabilirsiniz.</string>
|
||||
<string name="remote_hosts_section">Uzaktan kumandalı cep telefonları</string>
|
||||
<string name="member_criteria_off">kapalı</string>
|
||||
<string name="admission_stage_review">İnceleme üyeleri</string>
|
||||
<string name="admission_stage_review_descr">Kabul etmeden önce üyeleri inceleyin (kapıyı çalın).</string>
|
||||
@@ -2428,7 +2413,6 @@
|
||||
<string name="connection_error_quota_desc">Bağlantı, teslim edilemeyen mesajların sınırına ulaştı, bağlantınız çevrimdışı olabilir.</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">Bu bağlantı için daha yeni bir uygulama sürümü gereklidir. Lütfen uygulamayı güncelleyin veya irtibat kişinizden uyumlu bir bağlantı göndermesini isteyin.</string>
|
||||
<string name="report_reason_alert_title">Rapor nedeni?</string>
|
||||
<string name="report_archive_alert_desc">Rapor sizin için arşivlenecektir.</string>
|
||||
<string name="no_unread_chats">Okunmamış sohbet yok</string>
|
||||
<string name="notification_group_report">Rapor: %s</string>
|
||||
<string name="chat_list_group_reports">Raporlar</string>
|
||||
@@ -2480,7 +2464,6 @@
|
||||
<string name="compose_view_send_request">Talep gönder</string>
|
||||
<string name="compose_view_send_request_without_message">Mesaj olmadan istek gönder</string>
|
||||
<string name="v6_4_support_chat_descr">Özel geri bildirimlerinizi gruplara gönderin.</string>
|
||||
<string name="sent_to_your_contact_after_connection">Bağlantı kurulduktan sonra kişinize gönderilir.</string>
|
||||
<string name="share_group_profile_via_link">Grup linki güncellensin mi?</string>
|
||||
<string name="share_profile_via_link_alert_confirm">Güncelle</string>
|
||||
<string name="share_profile_via_link">Adres güncellensin mi?</string>
|
||||
@@ -2533,11 +2516,9 @@
|
||||
<string name="channel_subscriber_count_plural">%1$d aboneler</string>
|
||||
<string name="badge_supported_simplex">%1$s SimpleX Chat’i destekledi. Rozet %2$s tarihinde geçerliliğini yitirdi.</string>
|
||||
<string name="v6_4_1_new_interface_languages">4 yeni arayüz dili</string>
|
||||
<string name="settings_section_title_about">Hakkında</string>
|
||||
<string name="relay_status_accepted">kabul edildi</string>
|
||||
<string name="relay_status_acknowledged_roster">onaylanan kadro</string>
|
||||
<string name="relay_status_active">aktif</string>
|
||||
<string name="add_button">Ekle</string>
|
||||
<string name="add_relay_button">Röle ekle</string>
|
||||
<string name="add_relays_title">Röleler ekle</string>
|
||||
<string name="relay_bar_owner_no_delivery">Mesaj iletimini geri yüklemek için röle ekleyin.</string>
|
||||
@@ -2579,7 +2560,6 @@
|
||||
<string name="channel_no_active_relays_try_later">Kanalda aktif röle bulunmuyor. Lütfen daha sonra katılmayı deneyin.</string>
|
||||
<string name="chat_link_channel">Kanal bağlantısı</string>
|
||||
<string name="channel_link">Kanal bağlantısı</string>
|
||||
<string name="button_channel_members">Kanal üyeleri</string>
|
||||
<string name="channel_display_name_field">Kanal adı</string>
|
||||
<string name="channel_preferences">Kanal tercihleri</string>
|
||||
<string name="channel_profile_is_stored_on_subscribers_devices">Kanal profili abonelerin cihazlarında ve sohbet rölelerinde saklanır.</string>
|
||||
@@ -2609,8 +2589,6 @@
|
||||
<string name="relay_test_step_connect">Bağlan</string>
|
||||
<string name="relay_conn_status_connected">bağlı</string>
|
||||
<string name="relay_conn_status_connecting">bağlanılıyor</string>
|
||||
<string name="channel_name_requires_newer_app_version">Kanal adı üzerinden bağlanmak için daha yeni bir uygulama sürümü gerekir.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Kişi adı ile bağlanmak için daha yeni bir uygulama sürümü gerekir.</string>
|
||||
<string name="info_row_connection_failed">Bağlantı başarısız</string>
|
||||
<string name="connect_via_link_or_qr_code">Bağlantı veya kare kod ile bağlanın</string>
|
||||
<string name="settings_section_title_contact">İletişim</string>
|
||||
@@ -2658,7 +2636,6 @@
|
||||
<string name="error_creating_channel">Kanal oluşturulurken hata oluştu</string>
|
||||
<string name="error_deleting_message">Mesaj silinirken hata oluştu</string>
|
||||
<string name="error_marking_member_support_chat_read">Okundu olarak işaretlenirken hata</string>
|
||||
<string name="error_opening_channel">Kanal açılırken hata oluştu</string>
|
||||
<string name="rcv_msg_error_parse">hata: %s</string>
|
||||
<string name="error_saving_channel_profile">Kanal profili kaydedilirken hata oluştu</string>
|
||||
<string name="error_saving_simplex_name">İsim kaydedilirken hata oluştu</string>
|
||||
@@ -2668,7 +2645,6 @@
|
||||
<string name="relay_status_failed">başarısız oldu</string>
|
||||
<string name="content_filter_files">Dosyalar</string>
|
||||
<string name="files_prohibited_in_this_chat">Bu sohbette dosya ve medya yasaktır.</string>
|
||||
<string name="content_filter_menu_item">Filtre</string>
|
||||
<string name="proxy_destination_error_unknown_ca">Hedef sunucu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string>
|
||||
<string name="smp_proxy_error_unknown_ca">Yönlendirme sunucusu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string>
|
||||
<string name="network_error_unknown_ca">Sunucu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string>
|
||||
@@ -2741,14 +2717,12 @@
|
||||
<string name="privacy_chat_list_open_clean_web_link">Temiz bağlantıyı aç</string>
|
||||
<string name="open_external_link_title">Harici bağlantı açılsın mı?</string>
|
||||
<string name="privacy_chat_list_open_full_web_link">Tam bağlantıyı aç</string>
|
||||
<string name="connect_plan_open_new_channel">Yeni kanal açın</string>
|
||||
<string name="v6_5_safe_web_links_descr">- bağlantı önizlemelerini göndermeyi tercih edin.\n- etkinleştirilmişse SOCKS proxy kullanın.\n- köprü kimlik avını önleyin.\n- bağlantı izlemeyi kaldırın.</string>
|
||||
<string name="onboarding_or_show_qr_code">Ya da kare kodu şahsen veya görüntülü arama yoluyla gösterin.</string>
|
||||
<string name="onboarding_or_use_qr_code">Ya da bu kare kodu kullanın - yazdırın veya çevrimiçi gösterin.</string>
|
||||
<string name="member_info_section_title_owner">Sahip</string>
|
||||
<string name="channel_members_section_owners">Sahipler ve katkıda bulunanlar</string>
|
||||
<string name="v6_5_ownership">Sahiplik: kendi aktarıcılarınızı çalıştırabilirsiniz.</string>
|
||||
<string name="please_upgrade_the_app">Lütfen uygulamayı güncelleyin.</string>
|
||||
<string name="preset_relay_address">Önceden ayarlanmış aktarıcı adresi</string>
|
||||
<string name="preset_relay_name">Önceden ayarlanmış aktarıcı adı</string>
|
||||
<string name="v6_5_privacy">Gizlilik: sahipler ve aboneler için.</string>
|
||||
@@ -2813,7 +2787,6 @@
|
||||
<string name="owner_verification_failed">⚠️ İmza doğrulama başarısız oldu: %s.</string>
|
||||
<string name="tray_tooltip">SimpleX</string>
|
||||
<string name="tray_tooltip_unread">SimpleX - %d okunmamış</string>
|
||||
<string name="simplex_name">SimpleX adı</string>
|
||||
<string name="simplex_name_error">SimpleX ad hatası</string>
|
||||
<string name="simplex_name_not_verified">SimpleX adı doğrulanmadı</string>
|
||||
<string name="badge_invested">SimpleX Chat kitlesel fonlamasına yatırım yapan %s\'ler.</string>
|
||||
@@ -2858,15 +2831,12 @@
|
||||
<string name="relay_address_alert_message">Bu bir sohbet aktarım adresidir, bağlanmak için kullanılamaz.</string>
|
||||
<string name="last_active_relay_warning">Bu son aktif aktarıcıdır. Kaldırılması abonelere mesaj iletimini engelleyecektir.</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Bu <b>%1$s</b> kanalı için bağlantınız!]]></string>
|
||||
<string name="this_setting_is_for_your_current_profile">Bu ayar mevcut profiliniz içindir</string>
|
||||
<string name="simplex_name_not_found_desc">Bu SimpleX adı kayıtlı değil. Lütfen ismi kontrol edin.</string>
|
||||
<string name="v6_5_non_profit_governance_descr">SimpleX Ağını kalıcı kılmak için.</string>
|
||||
<string name="one_hand_ui_top_bar">Üst çubuk</string>
|
||||
<string name="operator_use_for_names">İsimleri çözümlemek için</string>
|
||||
<string name="unblock_subscriber_for_all_question">Herkes için abone engeli kaldırılsın mı?</string>
|
||||
<string name="simplex_name_unconfirmed">Onaylanmamış isim</string>
|
||||
<string name="unsupported_channel_name">Desteklenmeyen kanal adı</string>
|
||||
<string name="unsupported_contact_name">Desteklenmeyen kişi adı</string>
|
||||
<string name="badge_unverified_title">Doğrulanmamış rozet</string>
|
||||
<string name="rcv_channel_event_updated_channel_profile">güncellenen kanal profili</string>
|
||||
<string name="v6_4_1_short_address_update">Adresinizi güncelleyin</string>
|
||||
|
||||
@@ -1710,7 +1710,6 @@
|
||||
<string name="v5_7_forward_descr">Джерело повідомлення залишається приватним.</string>
|
||||
<string name="network_smp_proxy_mode_always">Завжди</string>
|
||||
<string name="network_smp_proxy_mode_always_description">Завжди використовуйте приватну маршрутизацію.</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Режим маршрутизації повідомлень</string>
|
||||
<string name="network_smp_proxy_mode_never_description">НЕ використовуйте приватну маршрутизацію.</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">Ні</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</string>
|
||||
@@ -1738,7 +1737,6 @@
|
||||
<string name="group_members_can_send_simplex_links">Учасники можуть надсилати посилання SimpleX.</string>
|
||||
<string name="v5_7_call_sounds">Звуки вхідного дзвінка</string>
|
||||
<string name="chat_theme_apply_to_light_mode">Світлий режим</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Запасний варіант маршрутизації повідомлень</string>
|
||||
<string name="settings_section_title_private_message_routing">Маршрутизація приватних повідомлень</string>
|
||||
<string name="forwarded_description">переслано</string>
|
||||
<string name="network_type_other">Інше</string>
|
||||
@@ -1791,7 +1789,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow_description">Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</string>
|
||||
<string name="chat_theme_set_default_theme">Встановлення теми за замовчуванням</string>
|
||||
<string name="color_sent_quote">Надіслано відповідь</string>
|
||||
<string name="chat_list_always_visible">Показати список чату в новому вікні</string>
|
||||
<string name="network_smp_proxy_mode_unknown_description">Використовуйте приватну маршрутизацію з невідомими серверами.</string>
|
||||
<string name="network_smp_proxy_mode_unprotected_description">Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена.</string>
|
||||
<string name="color_wallpaper_background">Фон шпалер</string>
|
||||
@@ -1876,7 +1873,6 @@
|
||||
<string name="v6_0_new_chat_experience">Новий досвід чату 🎉</string>
|
||||
<string name="server_address">Адреса сервера</string>
|
||||
<string name="confirm_delete_contact_question">Підтвердити видалення контакту?</string>
|
||||
<string name="info_view_connect_button">підключитися</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Сервер переадресації %1$s не зміг з\'єднатися з цільовим сервером %2$s. Будь ласка, спробуйте пізніше.</string>
|
||||
<string name="member_inactive_desc">Повідомлення може бути доставлено пізніше, якщо учасник стане активним.</string>
|
||||
<string name="selected_chat_items_nothing_selected">Нічого не вибрано</string>
|
||||
@@ -1991,7 +1987,6 @@
|
||||
<string name="info_view_message_button">повідомлення</string>
|
||||
<string name="info_view_call_button">дзвінок</string>
|
||||
<string name="keep_conversation">Зберегти розмову</string>
|
||||
<string name="info_view_open_button">відкрити</string>
|
||||
<string name="info_view_search_button">пошук</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">Ви все ще можете переглядати розмову з %1$s у списку чатів.</string>
|
||||
<string name="contact_deleted">Контакт видалено!</string>
|
||||
@@ -2130,17 +2125,12 @@
|
||||
<string name="or_to_share_privately">Або поділитися приватно</string>
|
||||
<string name="onboarding_choose_server_operators">Оператори серверів</string>
|
||||
<string name="onboarding_network_operators">Мережеві оператори</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Умови будуть прийняті для ввімкнених операторів через 30 днів.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Виберіть мережевих операторів для використання.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">Ви можете налаштувати сервери за допомогою налаштувань.</string>
|
||||
<string name="onboarding_network_operators_review_later">Перегляньте пізніше</string>
|
||||
<string name="onboarding_network_operators_update">Оновлення</string>
|
||||
<string name="operator_conditions_accepted">Прийняті умови</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Умови будуть автоматично прийняті для увімкнених операторів: %s.</string>
|
||||
<string name="operator_info_title">Оператор мережі</string>
|
||||
<string name="operator_servers_title">%s сервери</string>
|
||||
<string name="operator_website">Вебсайт</string>
|
||||
<string name="your_servers">Ваші сервери</string>
|
||||
<string name="use_servers_of_operator_x">Використовувати %s</string>
|
||||
<string name="operator_use_operator_toggle_description">Використовувати сервери</string>
|
||||
@@ -2187,7 +2177,6 @@
|
||||
<string name="onboarding_network_operators_continue">Продовжити</string>
|
||||
<string name="create_1_time_link">Створити одноразове посилання</string>
|
||||
<string name="error_updating_server_title">Помилка оновлення сервера</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">для кращої конфіденційності метаданих.</string>
|
||||
<string name="v6_2_network_decentralization">Децентралізація мережі</string>
|
||||
<string name="operator">Оператор</string>
|
||||
<string name="no_message_servers_configured_for_receiving">Немає серверів для отримання повідомлень.</string>
|
||||
@@ -2201,12 +2190,9 @@
|
||||
<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="connection_error_quota_desc">З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн.</string>
|
||||
<string name="address_creation_instruction">Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові.</string>
|
||||
<string name="operator_use_for_messages">Використовувати для повідомлень</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Ви можете налаштувати операторів у налаштуваннях Мережі та серверів.</string>
|
||||
<string name="chat_archive">Або імпортуйте архівний файл</string>
|
||||
<string name="remote_hosts_section">Віддалені мобільні</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Пристрої Xiaomi</b>: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]></string>
|
||||
<string name="maximum_message_size_title">Повідомлення занадто велике!</string>
|
||||
<string name="maximum_message_size_reached_non_text">Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову.</string>
|
||||
@@ -2293,7 +2279,6 @@
|
||||
<string name="report_item_visibility_moderators">Тільки відправник і модератори бачать це</string>
|
||||
<string name="report_verb">Повідомити</string>
|
||||
<string name="report_compose_reason_header_community">Повідомити про порушення: тільки модератори групи побачать це.</string>
|
||||
<string name="report_archive_alert_desc">Повідомлення буде архівоване для вас.</string>
|
||||
<string name="report_reason_other">Інша причина</string>
|
||||
<string name="report_item_archived">архівование повідомлення</string>
|
||||
<string name="archive_verb">Архів</string>
|
||||
@@ -2335,7 +2320,6 @@
|
||||
<string name="network_smp_web_port_footer">Використовувати TCP-порт %1$s, якщо порт не вказано.</string>
|
||||
<string name="network_smp_web_port_toggle">Використовувати веб-порт</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="report_archive_alert_desc_all">Усі звіти будуть архівовані для вас.</string>
|
||||
<string name="report_archive_alert_title_all">Архівувати всі звіти?</string>
|
||||
@@ -2485,7 +2469,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="v6_4_1_welcome_contacts_descr">Налаштуйте біографію профілю та вітальне повідомлення.</string>
|
||||
<string name="share_old_address_alert_button">Поділіться старою адресою</string>
|
||||
<string name="share_old_link_alert_button">Поділіться старим посиланням</string>
|
||||
@@ -2550,11 +2533,9 @@
|
||||
<string name="channel_subscriber_count_singular">%1$d підписник</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d підписники</string>
|
||||
<string name="badge_supported_simplex">%1$s підтримував SimpleX Chat. Завершення дії значка %2$s.</string>
|
||||
<string name="settings_section_title_about">Про</string>
|
||||
<string name="relay_status_accepted">підтверджено</string>
|
||||
<string name="relay_status_acknowledged_roster">затверджений список</string>
|
||||
<string name="relay_status_active">активний</string>
|
||||
<string name="add_button">Додати</string>
|
||||
<string name="add_relay_button">Додати перемикач</string>
|
||||
<string name="add_relays_title">Додати перемикачі</string>
|
||||
<string name="relay_bar_owner_no_delivery">Додайте перемикачі для відновлення доставки повідомлень.</string>
|
||||
@@ -2592,7 +2573,6 @@
|
||||
<string name="channel_no_active_relays_try_later">Канал не має активних перемикачів. Будь ласка, спробуйте підключитися пізніше.</string>
|
||||
<string name="channel_link">Посилання на канал</string>
|
||||
<string name="chat_link_channel">Посилання на канал</string>
|
||||
<string name="button_channel_members">Учасники каналу</string>
|
||||
<string name="channel_display_name_field">Імʼя каналу</string>
|
||||
<string name="channel_preferences">Налаштування каналів</string>
|
||||
<string name="channel_profile_is_stored_on_subscribers_devices">Профіль каналу зберігається на пристроях підписників та на ретрансляторах чату.</string>
|
||||
@@ -2622,8 +2602,6 @@
|
||||
<string name="relay_test_step_connect">Підключитися</string>
|
||||
<string name="relay_conn_status_connected">підключено</string>
|
||||
<string name="relay_conn_status_connecting">підключення</string>
|
||||
<string name="channel_name_requires_newer_app_version">Для підключення за назвою каналу потрібна новіша версія додатка.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Для підключення за іменем контакту потрібна новіша версія додатка.</string>
|
||||
<string name="info_row_connection_failed">Підключення не вдалося</string>
|
||||
<string name="connect_via_link_or_qr_code">Підключитися через посилання або QR код</string>
|
||||
<string name="settings_section_title_contact">Контакт</string>
|
||||
@@ -2648,7 +2626,6 @@
|
||||
<string name="enter_webpage_url">Введіть URL веб-сторінки</string>
|
||||
<string name="error_prefix">Помилка</string>
|
||||
<string name="error_deleting_message">Помилка видалення повідомлення</string>
|
||||
<string name="error_opening_channel">Помилка відкриття каналу</string>
|
||||
<string name="rcv_msg_error_parse">помилка: %s</string>
|
||||
<string name="error_saving_channel_profile">Помилка збереження профілю каналу</string>
|
||||
<string name="delete_relay">Видалити реле</string>
|
||||
@@ -2674,7 +2651,6 @@
|
||||
<string name="relay_conn_status_failed">помилка</string>
|
||||
<string name="relay_status_failed">помилка</string>
|
||||
<string name="content_filter_files">Файли</string>
|
||||
<string name="content_filter_menu_item">Фільтр</string>
|
||||
<string name="proxy_destination_error_unknown_ca">Відбиток в адресі цільового сервера не збігається із сертифікатом: %1$s.</string>
|
||||
<string name="for_anyone_to_reach_you">Щоб будь-хто міг з вами зв’язатися</string>
|
||||
<string name="from_history">З історії</string>
|
||||
|
||||
@@ -537,7 +537,6 @@
|
||||
<string name="member_info_member_disabled">đã bị tắt</string>
|
||||
<string name="disappearing_messages_are_prohibited">Tin nhắn tự xóa bị cấm.</string>
|
||||
<string name="info_view_call_button">gọi</string>
|
||||
<string name="info_view_connect_button">kết nối</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Liên hệ sẽ bị xóa - điều này không thể hoàn tác!</string>
|
||||
<string name="conversation_deleted">Cuộc trò chuyện đã bị xóa!</string>
|
||||
<string name="privacy_media_blur_radius">Làm mờ đa phương tiện</string>
|
||||
@@ -1007,7 +1006,6 @@
|
||||
<string name="v5_2_disappear_one_message">Làm cho một tin nhắn biến mất</string>
|
||||
<string name="live_message">Tin nhắn động!</string>
|
||||
<string name="keep_unused_invitation_question">Giữ lại lời mời chưa sử dụng?</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">Dự phòng định tuyến tin nhắn</string>
|
||||
<string name="invite_friends">Mời bạn bè</string>
|
||||
<string name="mark_code_verified">Đánh dấu đã xác thực</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Điều này có thể xảy ra khi:\n1. Tin nhắn hết hạn sau 2 ngày trên máy gửi hoặc sau 30 ngày trên máy chủ.\n2. Quá trình giải mã tin nhắn thất bại do bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ.\n3. Kết nối bị xâm phạm.</string>
|
||||
@@ -1052,7 +1050,6 @@
|
||||
<string name="settings_section_title_messages">Tin nhắn và tệp</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Việc xóa tin nhắn mà không thể phục hồi là bị cấm.</string>
|
||||
<string name="v5_5_join_group_conversation">Tham gia vào các cuộc trò chuyện nhóm</string>
|
||||
<string name="update_network_smp_proxy_mode_question">Chế độ định tuyến tin nhắn</string>
|
||||
<string name="email_invite_subject">Hãy trò chuyện trên SimpleX Chat</string>
|
||||
<string name="italic_text">in nghiêng</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Nó ảnh hưởng đến pin như thế nào</string>
|
||||
@@ -1235,7 +1232,6 @@
|
||||
<string name="opening_database">Đang mở cơ sở dữ liệu…</string>
|
||||
<string name="or_show_this_qr_code">Hoặc hiển thị mã này</string>
|
||||
<string name="only_your_contact_can_make_calls">Chỉ liên hệ của bạn mới có thể thực hiện cuộc gọi.</string>
|
||||
<string name="info_view_open_button">mở</string>
|
||||
<string name="v5_4_more_things_descr">- tùy chọn thông báo khi xóa liên hệ.\n- tên hồ sơ với dấu cách.\n- và hơn thế nữa!</string>
|
||||
<string name="permissions_open_settings">Mở cài đặt</string>
|
||||
<string name="migrate_from_device_or_share_this_file_link">Hoặc chia sẻ đường dẫn tệp này một cách an toàn.</string>
|
||||
@@ -1387,7 +1383,6 @@
|
||||
<string name="errors_in_servers_configuration">Lỗi trong cấu hình máy chủ.</string>
|
||||
<string name="connection_security">Bảo mật kết nối</string>
|
||||
<string name="create_1_time_link">Tạo đường dẫn dùng một lần</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Các điều kiện sẽ được chấp nhận với các nhà cung cấp được cho phép sau 30 ngày.</string>
|
||||
<string name="onboarding_network_operators_continue">Tiếp tục</string>
|
||||
<string name="operator_conditions_failed_to_load">Không thể tải văn bản về các điều kiện hiện tại, bạn có thể xem xét các điều kiện thông qua đường dẫn này:</string>
|
||||
<string name="operator_conditions_of_use">Các điều kiện sử dụng</string>
|
||||
@@ -1421,7 +1416,6 @@
|
||||
<string name="invite_to_chat_button">Mời để trò chuyện</string>
|
||||
<string name="button_leave_chat">Rời cuộc trò chuyện</string>
|
||||
<string name="operator_use_for_messages_private_routing">Cho định tuyến riêng tư</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">để bảo mật siêu dữ liệu tốt hơn.</string>
|
||||
<string name="v6_2_improved_chat_navigation">Đã cải thiện điều hướng trò chuyện</string>
|
||||
<string name="button_delete_chat">Xóa cuộc trò chuyện</string>
|
||||
<string name="for_social_media">Cho mạng xã hội</string>
|
||||
@@ -1532,7 +1526,6 @@
|
||||
<string name="refresh_qr_code">Làm mới</string>
|
||||
<string name="v4_5_reduced_battery_usage">Đã giảm mức sử dụng pin</string>
|
||||
<string name="reject_contact_button">Từ chối</string>
|
||||
<string name="remote_hosts_section">Các thiết bị di động từ xa</string>
|
||||
<string name="remove_passphrase">Xóa</string>
|
||||
<string name="remove_member_confirmation">Xóa</string>
|
||||
<string name="button_remove_member">Xóa thành viên</string>
|
||||
@@ -1617,7 +1610,6 @@
|
||||
<string name="compose_send_direct_message_to_connect">Gửi tin nhắn trực tiếp để kết nối</string>
|
||||
<string name="compose_save_messages_n">Đang lưu %1$s tin nhắn</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(quét hoặc dán từ bảng nháp)</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Chọn các bên vận hành mạng lưới để sử dụng.</string>
|
||||
<string name="button_send_direct_message">Gửi tin nhắn trực tiếp</string>
|
||||
<string name="save_welcome_message_question">Lưu lời chào?</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">gửi thất bại</string>
|
||||
@@ -1628,7 +1620,6 @@
|
||||
<string name="save_list">Lưu danh sách</string>
|
||||
<string name="retry_verb">Thử lại</string>
|
||||
<string name="save_and_notify_contact">Lưu và thông báo tới liên hệ</string>
|
||||
<string name="onboarding_network_operators_review_later">Xem xét lại sau</string>
|
||||
<string name="rcv_conn_event_verification_code_reset">mã bảo mật đã thay đổi</string>
|
||||
<string name="save_group_profile">Lưu hồ sơ nhóm</string>
|
||||
<string name="network_options_save_and_reconnect">Lưu và kết nối lại</string>
|
||||
@@ -1750,7 +1741,6 @@
|
||||
<string name="terminal_always_visible">Hiển thị bảng điều khiển trong cửa sổ mới</string>
|
||||
<string name="error_creating_report">Lỗi tạo báo cáo</string>
|
||||
<string name="share_message">Chia sẻ tin nhắn…</string>
|
||||
<string name="chat_list_always_visible">Hiển thị danh sách trò chuyện trong cửa sổ mới</string>
|
||||
<string name="notification_preview_mode_contact_desc">Chỉ hiển thị liên hệ</string>
|
||||
<string name="share_this_1_time_link">Chia sẻ đường dẫn dùng 1 lần này</string>
|
||||
<string name="share_with_contacts">Chia sẻ với các liên hệ</string>
|
||||
@@ -1902,7 +1892,6 @@
|
||||
<string name="la_mode_system">Hệ thống</string>
|
||||
<string name="whats_new_thanks_to_users_contribute_weblate">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string>
|
||||
<string name="contact_tap_to_connect">Nhấn để Kết nối</string>
|
||||
<string name="address_creation_instruction">Nhấn Tạo địa chỉ SimpleX ở menu để tạo sau.</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Nhấn để tham gia một cách ẩn danh</string>
|
||||
<string name="theme_system">Hệ thống</string>
|
||||
<string name="v6_1_better_calls_descr">Chuyển đổi âm thanh và video trong lúc gọi.</string>
|
||||
@@ -1977,7 +1966,6 @@
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Bản lưu trữ cơ sở dữ liệu đã được tải lên sẽ bị xóa vĩnh viễn khỏi các máy chủ.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Việc này không thể được hoàn tác - hồ sơ, các liên hệ, tin nhắn và tệp của bạn sẽ biến mất mà không thể khôi phục.</string>
|
||||
<string name="smp_servers_per_user">Những máy chủ cho các kết nối mới của hồ sơ trò chuyện hiện tại của bạn</string>
|
||||
<string name="report_archive_alert_desc">Báo cáo sẽ được lưu trữ cho bạn.</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">Tin nhắn này đã bị xóa hoặc vẫn chưa được nhận.</string>
|
||||
<string name="this_QR_code_is_not_a_link">Mã QR này không phải là một đường dẫn!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Đường dẫn này không phải là một đường dẫn kết nối hợp lệ!</string>
|
||||
@@ -2067,7 +2055,6 @@
|
||||
<string name="v6_0_upgrade_app">Nâng cấp ứng dụng một cách tự động</string>
|
||||
<string name="servers_info_uploaded">Đã tải lên</string>
|
||||
<string name="update_network_settings_confirmation">Cập nhật</string>
|
||||
<string name="onboarding_network_operators_update">Cập nhật</string>
|
||||
<string name="auth_unlock">Mở khóa</string>
|
||||
<string name="v4_4_verify_connection_security">Xác minh bảo mật kết nối</string>
|
||||
<string name="operator_use_for_files">Sử dụng cho các tệp</string>
|
||||
@@ -2131,7 +2118,6 @@
|
||||
<string name="description_via_contact_address_link">qua đường dẫn địa chỉ liên lạc</string>
|
||||
<string name="video_descr">Video</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Chúng tôi không lưu bất kỳ liên hệ hay tin nhắn nào của bạn (một khi đã được gửi) trên các máy chủ.</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="personal_welcome">Xin chào %1$s!</string>
|
||||
<string name="network_use_onion_hosts_prefer">Khi có sẵn</string>
|
||||
<string name="voice_messages_prohibited">Các tin nhắn thoại bị cấm!</string>
|
||||
@@ -2204,7 +2190,6 @@
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Bạn đã kết nối với %1$s rồi.</string>
|
||||
<string name="chat_preferences_you_allow">Bạn cho phép</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Bạn không được kết nối với các máy chủ này. Định tuyến riêng tư được sử dụng để gửi tin nhắn tới chúng.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">Bạn có thể định cấu hình các bên vận hành trong cài đặt Mạng & máy chủ.</string>
|
||||
<string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Bạn đang kết nối thông qua đường dẫn dùng một lần này!</string>
|
||||
<string name="migrate_from_device_try_again">Bạn có thể thử một lần nữa.</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Bạn được mời vào nhóm. Tham gia để kết nối với các thành viên nhóm.</string>
|
||||
@@ -2310,7 +2295,6 @@
|
||||
<string name="network_smp_web_port_toggle">Sử dụng cổng web</string>
|
||||
<string name="max_group_mentions_per_message_reached">Bạn có thể nhắc đến tối đa là %1$s thành viên trong một tin nhắn!</string>
|
||||
<string name="mute_all_chat">Tắt thông báo tất cả</string>
|
||||
<string name="unread_mentions">Những lời nhắc chưa đọc</string>
|
||||
<string name="group_members_can_send_reports">Các thành viên có thể báo cáo tin nhắn tới các kiểm duyệt viên.</string>
|
||||
<string name="report_archive_alert_desc_all">Tất cả các báo cáo sẽ được lưu trữ cho bạn.</string>
|
||||
<string name="report_archive_alert_title_all">Lưu trữ tất cả báo cáo?</string>
|
||||
@@ -2442,8 +2426,6 @@
|
||||
<string name="relay_conn_status_connected">đã kết nối</string>
|
||||
<string name="v6_4_connect_faster">Kết nối nhanh hơn 🚀</string>
|
||||
<string name="relay_conn_status_connecting">đang kết nối</string>
|
||||
<string name="channel_name_requires_newer_app_version">Kết nối bằng tên kênh yêu cầu phiên bản ứng dụng mới hơn</string>
|
||||
<string name="contact_name_requires_newer_app_version">Kết nối bằng tên liên hệ yêu cầu phiên bản ứng dụng mới hơn.</string>
|
||||
<string name="info_row_connection_failed">Kết nối thất bại</string>
|
||||
<string name="connect_via_link_or_qr_code">Kết nối bằng đường dẫn hoặc mã QR</string>
|
||||
<string name="settings_section_title_contact">Liên hệ</string>
|
||||
@@ -2465,7 +2447,6 @@
|
||||
<string name="error_creating_channel">Lỗi tạo kênh</string>
|
||||
<string name="error_deleting_message">Lỗi khi xoá tin nhắn</string>
|
||||
<string name="error_marking_member_support_chat_read">Lỗi khi đánh dấu \"đã xem\"</string>
|
||||
<string name="error_opening_channel">Lỗi mở kênh</string>
|
||||
<string name="error_preparing_contact">Lỗi khi mở đoạn chat</string>
|
||||
<string name="error_preparing_group">Lỗi khi mở nhóm</string>
|
||||
<string name="error_rejecting_contact_request">Lỗi khi từ chối lời mời kết nối</string>
|
||||
@@ -2481,13 +2462,11 @@
|
||||
<string name="let_someone_connect_to_you">Để ai đó kết nối với bạn</string>
|
||||
<string name="action_button_channel_link">Đường dẫn</string>
|
||||
<string name="loading_profile">Đang tải hồ sơ…</string>
|
||||
<string name="settings_section_title_about">Giới thiệu</string>
|
||||
<string name="accept_contact_request">Đồng ý yêu cầu kết nối</string>
|
||||
<string name="chat_banner_accept_contact_request">Đồng ý yêu cầu kết nối</string>
|
||||
<string name="relay_status_accepted">đã chấp nhận</string>
|
||||
<string name="relay_status_acknowledged_roster">danh sách đã xác nhận</string>
|
||||
<string name="relay_status_active">đang hoạt động</string>
|
||||
<string name="add_button">Thêm mới</string>
|
||||
<string name="compose_view_add_message">Thêm tin nhắn</string>
|
||||
<string name="add_relay_button">Thêm thiết bị</string>
|
||||
<string name="add_relays_title">Thêm mới thiết bị</string>
|
||||
|
||||
@@ -1763,7 +1763,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow">是</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit">不</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="private_routing_explanation">为了保护你的 IP 地址,私密路由使用你的 SMP 服务器来传送消息。</string>
|
||||
<string name="settings_section_title_private_message_routing">私密消息路由</string>
|
||||
@@ -1776,7 +1775,6 @@
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">不直接发送消息,即便你的服务器或目标服务器不支持私密路由。</string>
|
||||
<string name="snd_error_proxy_relay">转发服务器:%1$s
|
||||
\n目标服务器错误:%2$s</string>
|
||||
<string name="update_network_smp_proxy_mode_question">消息路由模式</string>
|
||||
<string name="file_not_approved_title">未知服务器!</string>
|
||||
<string name="file_not_approved_descr">没有 Tor 或 VPN,这些 XFTP 中继可以看到你的 IP 地址:
|
||||
\n%1$s.</string>
|
||||
@@ -1785,7 +1783,6 @@
|
||||
<string name="settings_section_title_files">文件</string>
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">应用将请求确认来自未知服务器的下载(.onion 或启用 SOCKS 代理时除外)。</string>
|
||||
<string name="settings_section_title_user_theme">个人资料主题</string>
|
||||
<string name="chat_list_always_visible">在新窗口中显示聊天列表</string>
|
||||
<string name="chat_theme_apply_to_all_modes">所有颜色模式</string>
|
||||
<string name="chat_theme_apply_to_mode">应用到</string>
|
||||
<string name="theme_black">黑</string>
|
||||
@@ -1994,10 +1991,8 @@
|
||||
<string name="cant_call_member_alert_title">无法呼叫群成员</string>
|
||||
<string name="cant_send_message_to_member_alert_title">无法给群成员发消息</string>
|
||||
<string name="confirm_delete_contact_question">确认删除联系人?</string>
|
||||
<string name="info_view_connect_button">连接</string>
|
||||
<string name="action_button_add_members">邀请</string>
|
||||
<string name="no_filtered_contacts">没有过滤的联系人</string>
|
||||
<string name="info_view_open_button">打开</string>
|
||||
<string name="info_view_search_button">搜索</string>
|
||||
<string name="cant_call_member_send_message_alert_text">发送消息来开启通话。</string>
|
||||
<string name="you_can_still_send_messages_to_contact">你可以发消息给来自已存档联系人的 %1$s。</string>
|
||||
@@ -2123,11 +2118,7 @@
|
||||
<string name="or_to_share_privately">或者私下分享</string>
|
||||
<string name="onboarding_choose_server_operators">服务器运营方</string>
|
||||
<string name="onboarding_network_operators">网络运营方</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">30 天后将接受已启用的运营方的条款。</string>
|
||||
<string name="onboarding_network_operators_continue">继续</string>
|
||||
<string name="onboarding_network_operators_review_later">稍后审阅</string>
|
||||
<string name="onboarding_select_network_operators_to_use">选择要使用的网络运营方。</string>
|
||||
<string name="onboarding_network_operators_update">更新</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">你可以通过设置配置服务器。</string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[将接受运营方的条款: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">将于下列日期自动接受已启用的运营方的条款:%s。</string>
|
||||
@@ -2137,7 +2128,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_failed_to_load">无法加载当前条款文本,你可以通过此链接审阅条款:</string>
|
||||
<string name="use_servers_of_operator_x">使用 %s</string>
|
||||
<string name="operator_use_operator_toggle_description">使用服务器</string>
|
||||
@@ -2180,7 +2170,6 @@
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[接受了运营方的条款: <b>%s</b>.]]></string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。</string>
|
||||
<string name="error_accepting_operator_conditions">接受条款出错</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">为了更好的元数据隐私。</string>
|
||||
<string name="error_adding_server">添加服务器出错</string>
|
||||
<string name="no_media_servers_configured">无媒体和文件服务器。</string>
|
||||
<string name="error_updating_server_title">更新服务器出错</string>
|
||||
@@ -2189,14 +2178,11 @@
|
||||
<string name="v6_2_improved_chat_navigation_descr">- 在第一条未读消息上打开聊天.\n- 跳转到引用的消息.</string>
|
||||
<string name="operator_review_conditions">审阅条款</string>
|
||||
<string name="simplex_address_or_1_time_link">SimpleX 地址或一次性链接?</string>
|
||||
<string name="address_creation_instruction">要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址”</string>
|
||||
<string name="xftp_servers_per_user">当前聊天资料的新文件服务器</string>
|
||||
<string name="message_deleted_or_not_received_error_desc">此消息被删除或尚未收到。</string>
|
||||
<string name="connection_error_quota_desc">连接达到了未送达消息上限,你的联系人可能处于离线状态。</string>
|
||||
<string name="to_protect_against_your_link_replaced_compare_codes">为了防止链接被替换,你可以比较联系人安全代码。</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">你可以在“网络和服务器”设置中配置运营方。</string>
|
||||
<string name="operator_conditions_accepted_on">接受运营方条款的日期:%s</string>
|
||||
<string name="remote_hosts_section">远程移动设备</string>
|
||||
<string name="chat_archive">或者导入存档文件</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>小米设备</b>:请在系统设置中开启“自动启动”让通知正常工作。]]></string>
|
||||
<string name="maximum_message_size_title">消息太大!</string>
|
||||
@@ -2294,7 +2280,6 @@
|
||||
<string name="report_reason_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="report_item_archived_by">存档了 %s 的举报</string>
|
||||
<string name="chat_list_group_reports">举报</string>
|
||||
<string name="group_reports_active_one">1 个举报</string>
|
||||
@@ -2324,7 +2309,6 @@
|
||||
<string name="network_smp_web_port_footer">未指定端口时,使用 TCP 端口 %1$s。</string>
|
||||
<string name="network_smp_web_port_toggle">使用 web 端口</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="report_archive_alert_desc_all">将为你存档所有举报。</string>
|
||||
<string name="report_archive_alert_title_all">存档所有举报?</string>
|
||||
@@ -2450,7 +2434,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>
|
||||
@@ -2504,7 +2487,6 @@
|
||||
<string name="connect_use_incognito_profile">使用隐身个人资料</string>
|
||||
<string name="v6_4_1_welcome_contacts">欢迎联系人👋</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="settings_section_title_contact_requests_from_groups">群内的联络请求</string>
|
||||
<string name="member_is_deleted_cant_accept_request">成员被删除——无法接受请求</string>
|
||||
<string name="allow_files_and_media_only_if">只有你的联系人允许的情况下才允许文件和媒体。</string>
|
||||
@@ -2536,7 +2518,6 @@
|
||||
<string name="remove_member_delete_messages_confirmation">移除并删除消息</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>
|
||||
@@ -2567,7 +2548,6 @@
|
||||
<string name="chat_banner_channel">频道</string>
|
||||
<string name="info_row_channel">频道</string>
|
||||
<string name="channel_link">频道链接</string>
|
||||
<string name="button_channel_members">频道成员</string>
|
||||
<string name="channel_display_name_field">频道名</string>
|
||||
<string name="delete_channel_for_all_subscribers_cannot_undo_warning">将为所有订阅者删除频道 —— 此操作无法撤销!</string>
|
||||
<string name="delete_channel_for_self_cannot_undo_warning">将为你删除频道 —— 此操作无法撤销!</string>
|
||||
@@ -2597,7 +2577,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="relay_conn_status_failed">失败</string>
|
||||
<string name="relay_status_failed">失败</string>
|
||||
<string name="relay_test_step_get_link">获取链接</string>
|
||||
@@ -2614,7 +2593,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>
|
||||
@@ -2792,7 +2770,6 @@
|
||||
<string name="one_hand_ui_top_bar">顶部栏</string>
|
||||
<string name="cancel_channel_alert_msg">你的新频道 %1$s 已连接到 %3$d 个中继中的 %2$d 个中继。\n如果取消,频道将被删除 —— 你可以再次创建它。</string>
|
||||
<string name="last_active_relay_warning">这是上次活跃的中继。删除它会阻止给订阅者传送消息。</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>
|
||||
@@ -2831,12 +2808,9 @@
|
||||
<string name="channel_owner_count_plural">%1$d 名所有者</string>
|
||||
<string name="channel_owners_contributors_count">%1$d 名所有者和贡献者</string>
|
||||
<string name="badge_supported_simplex">%1$s 支持过 SimpleX Chat。该徽章已于 %2$s 过期。</string>
|
||||
<string name="settings_section_title_about">关于</string>
|
||||
<string name="badge_unknown_key_title">无法验证徽章</string>
|
||||
<string name="channel_webpage">频道网页</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>
|
||||
@@ -2847,7 +2821,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="badge_invested">%s 给 SimpleX Chat 众筹投过钱。</string>
|
||||
<string name="badge_supports_simplex">%s 支持 SimpleX Chat。</string>
|
||||
<string name="group_member_role_observer_channel">订阅者</string>
|
||||
@@ -2855,8 +2828,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>
|
||||
@@ -2879,7 +2850,6 @@
|
||||
<string name="simplex_name_no_valid_link">无有效链接</string>
|
||||
<string name="simplex_name_resolver_error_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>
|
||||
|
||||
@@ -1437,7 +1437,6 @@
|
||||
<string name="files_and_media_prohibited">禁止檔案和媒體!</string>
|
||||
<string name="loading_remote_file_title">檔案載入中</string>
|
||||
<string name="file_error">檔案錯誤</string>
|
||||
<string name="update_network_smp_proxy_fallback_question">備用訊息路由</string>
|
||||
<string name="network_smp_proxy_fallback_prohibit_description">如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。</string>
|
||||
<string name="create_another_profile_button">建立個人檔案</string>
|
||||
<string name="migrate_from_another_device">從另一台裝置轉移</string>
|
||||
@@ -1533,7 +1532,6 @@
|
||||
<string name="error_aborting_address_change">中止地址更改時出錯</string>
|
||||
<string name="error_showing_desktop_notification">顯示通知出錯,請聯繫開發者。</string>
|
||||
<string name="delete_and_notify_contact">刪除並通知聯絡人</string>
|
||||
<string name="update_network_smp_proxy_mode_question">訊息路由模式</string>
|
||||
<string name="network_smp_proxy_mode_never_description">不使用私密路由。</string>
|
||||
<string name="permissions_grant">要進行通話請授予一項或多項權限</string>
|
||||
<string name="permissions_find_in_settings_and_grant">在 Android 系統設定中找到此權限並手動授予權限。</string>
|
||||
@@ -1664,7 +1662,6 @@
|
||||
<string name="member_inactive_title">成員非活躍</string>
|
||||
<string name="compose_message_placeholder">訊息</string>
|
||||
<string name="delete_messages_mark_deleted_warning">訊息將被標記為刪除。收信人可以揭示這些訊息。</string>
|
||||
<string name="info_view_connect_button">連接</string>
|
||||
<string name="info_view_message_button">訊息</string>
|
||||
<string name="app_check_for_updates_notice_disable">停用</string>
|
||||
<string name="app_check_for_updates_download_started">下載更新中,請不要關閉應用</string>
|
||||
@@ -1754,7 +1751,6 @@
|
||||
<string name="private_routing_error">私密路由出錯</string>
|
||||
<string name="message_forwarded_desc">尚無直接連接,訊息由管理員轉發。</string>
|
||||
<string name="selected_chat_items_nothing_selected">什麼也沒選中</string>
|
||||
<string name="info_view_open_button">開啟</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">私密路由</string>
|
||||
<string name="open_database_folder">打開資料庫文件夾</string>
|
||||
<string name="settings_section_title_private_message_routing">私密訊息路由</string>
|
||||
@@ -1859,7 +1855,6 @@
|
||||
<string name="network_smp_proxy_fallback_allow">是</string>
|
||||
<string name="audio_device_speaker">喇叭</string>
|
||||
<string name="unblock_member_confirmation">解除封鎖</string>
|
||||
<string name="operator_website">網站</string>
|
||||
<string name="appearance_in_app_bars_alpha">透明度</string>
|
||||
<string name="wallpaper_scale_repeat">重複</string>
|
||||
<string name="edit_chat_list_menu_action">編輯</string>
|
||||
@@ -1867,7 +1862,6 @@
|
||||
<string name="select_verb">選擇</string>
|
||||
<string name="network_proxy_username">使用者名稱</string>
|
||||
<string name="network_proxy_password">密碼</string>
|
||||
<string name="onboarding_network_operators_update">更新</string>
|
||||
<string name="onboarding_network_operators_continue">繼續</string>
|
||||
<string name="group_member_status_rejected">已拒絕</string>
|
||||
<string name="color_mode_system">系統</string>
|
||||
@@ -2028,7 +2022,6 @@
|
||||
<string name="cant_send_message_contact_deleted">聯絡人已刪除</string>
|
||||
<string name="cant_send_message_contact_not_synchronized">未同步</string>
|
||||
<string name="cant_send_message_contact_disabled">聯絡人已停用</string>
|
||||
<string name="onboarding_network_operators_review_later">稍後審核</string>
|
||||
<string name="new_chat_share_profile">分享個人檔案</string>
|
||||
<string name="full_link_button_text">完整連結</string>
|
||||
<string name="uploaded_files">已上載檔案</string>
|
||||
@@ -2077,7 +2070,6 @@
|
||||
<string name="snd_group_event_member_blocked">你已封鎖 %s</string>
|
||||
<string name="connection_not_ready">連接未就緒。</string>
|
||||
<string name="v6_1_message_dates_descr">更好的訊息日期。</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">為了更好的元資料隱私。</string>
|
||||
<string name="migrate_to_device_try_again">你可以再試一次。</string>
|
||||
<string name="connect_use_incognito_profile">使用匿名個人檔案</string>
|
||||
<string name="connect_plan_open_chat">開啟聊天</string>
|
||||
@@ -2137,7 +2129,6 @@
|
||||
<string name="content_filter_videos">影片</string>
|
||||
<string name="content_filter_files">檔案</string>
|
||||
<string name="content_filter_links">連結</string>
|
||||
<string name="content_filter_menu_item">過濾器</string>
|
||||
<string name="group_reports_active">%d 個舉報</string>
|
||||
<string name="deprecated_options_section">已棄用的選項</string>
|
||||
<string name="server_no_sub">無訂閱</string>
|
||||
@@ -2226,11 +2217,6 @@
|
||||
<string name="error_deleting_member_support_chat">刪除聊天時發生錯誤</string>
|
||||
<string name="unsupported_connection_link">不支援的連線連結</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">此連結需要較新的應用程式版本。請升級應用程式,或請你的聯絡人傳送相容的連結。</string>
|
||||
<string name="unsupported_channel_name">不支援的頻道名稱</string>
|
||||
<string name="unsupported_contact_name">不支援的聯絡人名稱</string>
|
||||
<string name="channel_name_requires_newer_app_version">透過頻道名稱連線需要較新的應用程式版本。</string>
|
||||
<string name="contact_name_requires_newer_app_version">透過聯絡人名稱連線需要較新的應用程式版本。</string>
|
||||
<string name="please_upgrade_the_app">請升級應用程式。</string>
|
||||
<string name="channel_temporarily_unavailable">頻道暫時無法使用</string>
|
||||
<string name="channel_no_active_relays_try_later">頻道沒有啟用中的中繼。請稍後再嘗試加入。</string>
|
||||
<string name="app_update_required">需要更新應用程式</string>
|
||||
@@ -2250,7 +2236,6 @@
|
||||
<string name="system_restricted_background_in_call_warn"><![CDATA[若要在背景中進行通話,請在應用程式設定中選擇<b>應用程式電池用量</b> / <b>不受限制</b>。]]></string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi 裝置</b>:請在系統設定中啟用自動啟動,以便通知正常運作。]]></string>
|
||||
<string name="message_deleted_or_not_received_error_desc">此訊息已刪除或尚未接收。</string>
|
||||
<string name="report_archive_alert_desc">該檢舉報告將為你封存。</string>
|
||||
<string name="report_archive_alert_desc_all">所有檢舉報告將為你封存。</string>
|
||||
<string name="snd_error_auth">金鑰錯誤或未知連線——此連線很可能已刪除。</string>
|
||||
<string name="srv_error_host">伺服器地址與網路設定不相容。</string>
|
||||
@@ -2273,7 +2258,6 @@
|
||||
<string name="for_anyone_to_reach_you">讓任何人都能聯絡你</string>
|
||||
<string name="no_chats_in_list">列表 %s 中沒有聊天。</string>
|
||||
<string name="contact_should_accept">聯絡人需要接受…</string>
|
||||
<string name="address_creation_instruction">稍後可點選選單中的「建立 SimpleX 地址」來建立。</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">沒有可轉發的內容!</string>
|
||||
<string name="forward_alert_forward_messages_without_files">要轉發不含檔案的訊息嗎?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">這些訊息在你選取後已被刪除。</string>
|
||||
@@ -2341,7 +2325,6 @@
|
||||
<string name="reject_contact_request">拒絕聯絡請求</string>
|
||||
<string name="the_sender_will_not_be_notified">傳送者不會收到通知。</string>
|
||||
<string name="member_is_deleted_cant_accept_request">成員已刪除,無法接受請求</string>
|
||||
<string name="unread_mentions">未讀提及</string>
|
||||
<string name="duplicated_list_error">所有列表的名稱和 emoji 都應不同。</string>
|
||||
<string name="delete_chat_list_warning">所有聊天都將從列表 %s 中移除,且該列表會被刪除</string>
|
||||
<string name="share_simplex_address_on_social_media">在社交媒體上分享 SimpleX 地址。</string>
|
||||
@@ -2380,7 +2363,6 @@
|
||||
<string name="app_check_for_updates_notice_desc">若要接收新版本通知,請開啟穩定版或 Beta 版的定期檢查。</string>
|
||||
<string name="show_slow_api_calls">顯示較慢的 API 呼叫</string>
|
||||
<string name="prefs_error_saving_settings">儲存設定時發生錯誤</string>
|
||||
<string name="sent_to_your_contact_after_connection">連線後會傳送給你的聯絡人。</string>
|
||||
<string name="or_to_share_privately">或私下分享</string>
|
||||
<string name="simplex_address_or_1_time_link">SimpleX 地址還是一次性連結?</string>
|
||||
<string name="new_1_time_link">新的一次性連結</string>
|
||||
@@ -2429,11 +2411,8 @@
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">應用程式會在每個對話中使用不同營運商,以保護你的私隱。</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">啟用多於一個營運商時,沒有任何一方擁有足以得知誰與誰通訊的中繼資料。</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">例如,如果你的聯絡人透過 SimpleX Chat 伺服器接收訊息,你的應用程式會透過 Flux 伺服器傳送訊息。</string>
|
||||
<string name="onboarding_select_network_operators_to_use">選擇要使用的網路營運商。</string>
|
||||
<string name="how_it_helps_privacy">這如何有助於私隱</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">你可以透過設定配置伺服器。</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">30 天後,將會接受已啟用營運商的條件。</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">你可以在「網路與伺服器」設定中配置營運商。</string>
|
||||
<string name="onboarding_your_network">你的網路</string>
|
||||
<string name="onboarding_network_routers_cannot_know">網路路由器無法知道\n誰在和誰交談</string>
|
||||
<string name="onboarding_configure_routers">設定路由器</string>
|
||||
@@ -2450,7 +2429,6 @@
|
||||
<string name="app_will_ask_to_confirm_unknown_file_servers">應用程式會要求確認來自未知檔案伺服器的下載(.onion 或啟用 SOCKS 代理時除外)。</string>
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">未使用 Tor 或 VPN 時,你的 IP 地址會對檔案伺服器可見。</string>
|
||||
<string name="sanitize_links_toggle">移除連結追蹤</string>
|
||||
<string name="this_setting_is_for_your_current_profile">此設定適用於你目前的個人檔案</string>
|
||||
<string name="these_settings_are_for_your_current_profile">這些設定適用於你目前的個人檔案</string>
|
||||
<string name="receipts_section_description_1">可在聯絡人和群組設定中覆寫這些設定。</string>
|
||||
<string name="receipts_contacts_override_enabled">已為 %d 個聯絡人啟用送達回條</string>
|
||||
@@ -2464,14 +2442,12 @@
|
||||
<string name="privacy_chat_list_open_clean_web_link">開啟乾淨連結</string>
|
||||
<string name="settings_section_title_delivery_receipts">傳送送達回條給</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">來自群組的聯絡請求</string>
|
||||
<string name="settings_section_title_about">關於</string>
|
||||
<string name="settings_section_title_contact">聯絡</string>
|
||||
<string name="settings_section_title_support_project">支持此專案</string>
|
||||
<string name="chat_data">聊天數據</string>
|
||||
<string name="help_and_support">說明與支援</string>
|
||||
<string name="more_privacy">更多私隱</string>
|
||||
<string name="advanced_settings">進階設定</string>
|
||||
<string name="remote_hosts_section">遠端行動裝置</string>
|
||||
<string name="chat_database_exported_save">你可以儲存匯出的封存檔。</string>
|
||||
<string name="chat_database_exported_migrate">你可以遷移匯出的數據庫。</string>
|
||||
<string name="chat_database_exported_not_all_files">部分檔案未匯出</string>
|
||||
@@ -2486,7 +2462,6 @@
|
||||
<string name="one_hand_ui_bottom_bar">底部列</string>
|
||||
<string name="one_hand_ui_top_bar">頂部列</string>
|
||||
<string name="terminal_always_visible">在新視窗中顯示控制台</string>
|
||||
<string name="chat_list_always_visible">在新視窗中顯示聊天列表</string>
|
||||
<string name="down_migration_warning_chat_relays">如果你加入或建立了頻道,它們將永久停止運作。</string>
|
||||
<string name="leave_channel_question">要離開頻道嗎?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">你將停止接收此頻道的訊息。聊天記錄將會保留。</string>
|
||||
@@ -2535,7 +2510,6 @@
|
||||
<string name="only_chat_owners_can_change_prefs">只有聊天擁有者可以變更偏好設定。</string>
|
||||
<string name="send_receipts_disabled_alert_msg">此群組有超過 %1$d 名成員,不會傳送送達回條。</string>
|
||||
<string name="action_button_channel_link">連結</string>
|
||||
<string name="button_channel_members">頻道成員</string>
|
||||
<string name="button_channel_relays">聊天中繼</string>
|
||||
<string name="button_remove_subscriber_question">要移除訂閱者嗎?</string>
|
||||
<string name="button_delete_member_messages_question">要刪除成員訊息嗎?</string>
|
||||
@@ -2827,7 +2801,6 @@
|
||||
<string name="your_profile_shared_with_channel_relays">你的個人檔案 %1$s 將與頻道中繼和訂閱者共享。\n中繼可以存取頻道訊息。</string>
|
||||
<string name="configure_relays">設定中繼</string>
|
||||
<string name="relay_status_failed">失敗</string>
|
||||
<string name="add_button">新增</string>
|
||||
<string name="add_relay_button">新增中繼</string>
|
||||
<string name="add_relays_title">新增中繼</string>
|
||||
<string name="no_available_relays">沒有可用的中繼</string>
|
||||
@@ -2843,10 +2816,8 @@
|
||||
<string name="relay_address_alert_title">中繼地址</string>
|
||||
<string name="relay_address_alert_message">這是聊天中繼地址,不能用於連接。</string>
|
||||
<string name="connect_plan_open_channel">開啟頻道</string>
|
||||
<string name="connect_plan_open_new_channel">開啟新頻道</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel">你的頻道</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[這是你用於頻道 <b>%1$s</b> 的連結!]]></string>
|
||||
<string name="error_opening_channel">開啟頻道時發生錯誤</string>
|
||||
<string name="unblock_subscriber_for_all_question">要對全部解除封鎖訂閱者嗎?</string>
|
||||
<string name="link_previews_alert_title">要啟用連結預覽嗎?</string>
|
||||
<string name="link_previews_alert_desc">傳送連結預覽可能會向網站透露你的 IP 地址。你可以稍後在「私隱」設定中變更此設定。</string>
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import BadgeService.Options (BadgeServiceOpts (..))
|
||||
import BadgeService.Service
|
||||
import Simplex.Chat.Terminal (terminalChatConfig)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
opts@BadgeServiceOpts {runCLI} <- welcomeGetOpts
|
||||
if runCLI
|
||||
then badgeServiceCLI opts
|
||||
else newServiceState >>= badgeService opts terminalChatConfig
|
||||
@@ -0,0 +1,60 @@
|
||||
# SimpleX badge service
|
||||
|
||||
Scaffolding for the SimpleX supporter-badge RPC service. The wire protocol is specified in [`docs/protocol/badges-rpc.md`](../../docs/protocol/badges-rpc.md), and the implementation plans live under [`plans/`](../../plans) (`2026-07-30-supporter-badges-v3-ux.md`, `2026-07-31-badges-core-implementation.md`, `2026-08-04-badges-mvp-scope.md`).
|
||||
|
||||
At this stage the service:
|
||||
|
||||
- creates a double-ratchet contact address on first start (service RPC requires DR, see [`docs/protocol/badges-rpc.md`](../../docs/protocol/badges-rpc.md)),
|
||||
- listens for service requests (`CEvtServiceRequest`) on that address, rejects a request whose `purchaseKey` is not the key the agent verified the signature against, and answers `redeemBadgeCode`,
|
||||
- issues redemption codes, storing only their `SHA-256` and printing each code once,
|
||||
- does not accept contact requests — the address is for RPC only,
|
||||
- owns the `sx_badge_service_`-prefixed tables and its own migrations table (`sx_badge_service_migrations`).
|
||||
|
||||
Every other command still answers `unsupported_version`. Ledger writes, invoices and provider webhooks are left for follow-up per the plans; a redemption issues one credential and reports an empty statement.
|
||||
|
||||
## Build
|
||||
|
||||
Build prerequisites and the general contribution flow are in [`docs/CONTRIBUTING.md`](../../docs/CONTRIBUTING.md).
|
||||
|
||||
```
|
||||
cabal build exe:simplex-badge-service
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
simplex-badge-service --help
|
||||
```
|
||||
|
||||
- default (no `--run-cli`): background service mode, no interactive terminal.
|
||||
- `--run-cli`: interactive CLI that also processes service requests (mirrors `simplex-directory-service --run-cli`).
|
||||
- `--no-address`: skip address creation on start-up (for operators who provision the address themselves).
|
||||
|
||||
The service cannot sign credentials without an issuer key and refuses to start without one:
|
||||
|
||||
- `--issuer-key-idx IDX` — the index the apps find the matching public key under (`badgePublicKeys` in `ChatConfig`).
|
||||
- `--issuer-secret SECRET` — the issuer secret from `simplex-chat badge keygen`.
|
||||
|
||||
The service checks the secret against the configured public key at that index and refuses to start
|
||||
if they disagree: credentials signed with the wrong key cannot be verified by any client, and the
|
||||
codes redeemed against them would be spent for nothing.
|
||||
|
||||
## Issuing codes
|
||||
|
||||
Issuing a code is an operator command sent to the running service in `--run-cli` mode, not a way
|
||||
to start it — so codes are issued without a second process touching the service's database:
|
||||
|
||||
```
|
||||
//issue <badge_type> [months] [paid|unpaid|free]
|
||||
//issue supporter 12
|
||||
```
|
||||
|
||||
`months` defaults to 1 and must be between 1 and 255; the status defaults to `free` and records
|
||||
whether the code was sold (`paid`), is awaiting payment (`unpaid`), or was issued by an operator
|
||||
(`free`) — redemption never reads it.
|
||||
|
||||
The code is printed once and only its `SHA-256` is stored, so a code that is not copied when it is
|
||||
shown cannot be recovered.
|
||||
|
||||
Core parses `//...` into `CustomChatCommand` and leaves it to the service's `preCmdHook`, which is
|
||||
why issuing codes lives in the service rather than in core.
|
||||
@@ -0,0 +1,126 @@
|
||||
{-# LANGUAGE ApplicativeDo #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module BadgeService.Options
|
||||
( BadgeServiceOpts (..),
|
||||
BadgeIssuerKey (..),
|
||||
getBadgeServiceOpts,
|
||||
badgeServiceOpts,
|
||||
mkChatOpts,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Text as T
|
||||
import Options.Applicative
|
||||
import Simplex.Chat.Controller (updateStr, versionNumber, versionString)
|
||||
import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP)
|
||||
import Simplex.Messaging.Crypto.BBS (BBSSecretKey)
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
|
||||
data BadgeServiceOpts = BadgeServiceOpts
|
||||
{ coreOptions :: CoreChatOpts,
|
||||
serviceName :: T.Text,
|
||||
clientService :: Bool,
|
||||
noAddress :: Bool,
|
||||
runCLI :: Bool,
|
||||
-- the service refuses to start without this: it cannot sign a credential
|
||||
issuerKey :: Maybe BadgeIssuerKey,
|
||||
testing :: Bool
|
||||
}
|
||||
|
||||
-- | The issuer secret that signs credentials, and the index the apps find its public half under.
|
||||
data BadgeIssuerKey = BadgeIssuerKey
|
||||
{ keyIdx :: Int,
|
||||
secretKey :: BBSSecretKey
|
||||
}
|
||||
|
||||
-- BBSSecretKey derives Show, so this is written out to keep the secret out of logs and errors
|
||||
instance Show BadgeIssuerKey where
|
||||
show BadgeIssuerKey {keyIdx} = "issuer key " <> show keyIdx
|
||||
|
||||
badgeServiceOpts :: FilePath -> FilePath -> Parser BadgeServiceOpts
|
||||
badgeServiceOpts appDir defaultDbName = do
|
||||
coreOptions <- coreChatOptsP appDir defaultDbName
|
||||
serviceName <-
|
||||
strOption
|
||||
( long "service-name"
|
||||
<> metavar "SERVICE_NAME"
|
||||
<> help "The display name of the badge service bot, without *'s and spaces (SimpleX Badges)"
|
||||
<> value "SimpleX Badges"
|
||||
)
|
||||
clientService <-
|
||||
switch
|
||||
( long "client-service"
|
||||
<> help "Use client service certificate"
|
||||
)
|
||||
noAddress <-
|
||||
switch
|
||||
( long "no-address"
|
||||
<> help "skip checking and creating service address"
|
||||
)
|
||||
runCLI <-
|
||||
switch
|
||||
( long "run-cli"
|
||||
<> help "Run badge service as CLI"
|
||||
)
|
||||
issuerKeyIdx <-
|
||||
optional $
|
||||
option
|
||||
auto
|
||||
( long "issuer-key-idx"
|
||||
<> metavar "KEY_IDX"
|
||||
<> help "Index of the issuer key in the app config (required with --issuer-secret)"
|
||||
)
|
||||
issuerSecret <-
|
||||
optional $
|
||||
option
|
||||
(eitherReader $ strDecode . B.pack)
|
||||
( long "issuer-secret"
|
||||
<> metavar "ISSUER_SECRET"
|
||||
<> help "Issuer secret from `simplex-chat badge keygen` (base64url)"
|
||||
)
|
||||
pure
|
||||
BadgeServiceOpts
|
||||
{ coreOptions,
|
||||
serviceName = T.pack serviceName,
|
||||
clientService,
|
||||
noAddress,
|
||||
runCLI,
|
||||
issuerKey = BadgeIssuerKey <$> issuerKeyIdx <*> issuerSecret,
|
||||
testing = False
|
||||
}
|
||||
|
||||
getBadgeServiceOpts :: FilePath -> FilePath -> IO BadgeServiceOpts
|
||||
getBadgeServiceOpts appDir defaultDbName =
|
||||
execParser $
|
||||
info
|
||||
(helper <*> versionOption <*> badgeServiceOpts appDir defaultDbName)
|
||||
(header versionStr <> fullDesc <> progDesc "Start SimpleX Badge Service with DB_FILE options")
|
||||
where
|
||||
versionStr = versionString versionNumber
|
||||
versionOption = infoOption versionAndUpdate (long "version" <> short 'v' <> help "Show version")
|
||||
versionAndUpdate = versionStr <> "\n" <> updateStr
|
||||
|
||||
mkChatOpts :: BadgeServiceOpts -> ChatOpts
|
||||
mkChatOpts BadgeServiceOpts {coreOptions, serviceName, clientService} =
|
||||
ChatOpts
|
||||
{ coreOptions,
|
||||
chatCmd = "",
|
||||
chatCmdDelay = 3,
|
||||
chatCmdLog = CCLNone,
|
||||
chatServerPort = Nothing,
|
||||
optFilesFolder = Nothing,
|
||||
optTempDirectory = Nothing,
|
||||
showReactions = False,
|
||||
showFullLinks = False,
|
||||
allowInstantFiles = True,
|
||||
autoAcceptFileSize = 0,
|
||||
muteNotifications = True,
|
||||
markRead = False,
|
||||
createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService},
|
||||
userDisplayName = Nothing,
|
||||
userImageFile = Nothing
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module BadgeService.Service
|
||||
( ServiceState (..),
|
||||
newServiceState,
|
||||
welcomeGetOpts,
|
||||
checkIssuerKey,
|
||||
badgeService,
|
||||
badgeServiceCLI,
|
||||
badgeServiceResponse,
|
||||
badgeErrorRetryAfter,
|
||||
IssueCodeOpts (..),
|
||||
issueBadgeCode,
|
||||
)
|
||||
where
|
||||
|
||||
import BadgeService.Options
|
||||
import BadgeService.Store
|
||||
import BadgeService.Store.Migrate (runBadgeServiceMigrations)
|
||||
import Control.Applicative (optional)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Char (isSpace)
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Maybe (fromMaybe, maybeToList)
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Badges
|
||||
import Simplex.Chat.Badges.Code
|
||||
import Simplex.Chat.Badges.Ledger
|
||||
import Simplex.Chat.Badges.Service
|
||||
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus (..))
|
||||
import Simplex.Chat.Bot (initializeBotAddress')
|
||||
import Simplex.Chat.Bot.Store (withDB, withDB')
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Core (sendChatCmd, simplexChatCore)
|
||||
import Simplex.Chat.Options (printDbOpts)
|
||||
import Simplex.Chat.Terminal (terminalChatConfig)
|
||||
import Simplex.Chat.Terminal.Main (simplexChatCLI')
|
||||
import Simplex.Chat.Types (AgentInvId (..), User (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BBS (bbsPublicKey)
|
||||
import Simplex.Messaging.Encoding.String (TextEncoding, strEncode, textDecode)
|
||||
import Simplex.Messaging.Version (isCompatible)
|
||||
import Simplex.Messaging.Util (raceAny_, safeDecodeUtf8, tshow)
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
data ServiceState = ServiceState
|
||||
{ serviceCC :: TMVar ChatController,
|
||||
serviceRequestQ :: TQueue (User, AgentInvId, Maybe C.PublicKeyEd25519, J.Object)
|
||||
}
|
||||
|
||||
newServiceState :: IO ServiceState
|
||||
newServiceState = do
|
||||
serviceCC <- newEmptyTMVarIO
|
||||
serviceRequestQ <- newTQueueIO
|
||||
pure ServiceState {serviceCC, serviceRequestQ}
|
||||
|
||||
welcomeGetOpts :: IO BadgeServiceOpts
|
||||
welcomeGetOpts = do
|
||||
appDir <- getAppUserDataDirectory "simplex"
|
||||
opts@BadgeServiceOpts {coreOptions, testing, serviceName} <- getBadgeServiceOpts appDir "simplex_badge_service"
|
||||
unless testing $ do
|
||||
putStrLn $ "SimpleX Badge Service v" ++ versionNumber
|
||||
printDbOpts coreOptions
|
||||
putStrLn $ "Service name: " ++ T.unpack serviceName
|
||||
pure opts
|
||||
|
||||
-- | Check the secret is the key trusted at its index: otherwise every code redeemed is burned.
|
||||
checkIssuerKey :: BadgeServiceOpts -> ChatConfig -> IO (Either String BadgeIssuerKey)
|
||||
checkIssuerKey BadgeServiceOpts {issuerKey} ChatConfig {badgePublicKeys} = case issuerKey of
|
||||
Nothing -> pure $ Left "an issuer key is required - pass both --issuer-key-idx and --issuer-secret (see `simplex-chat badge keygen`)"
|
||||
Just k@BadgeIssuerKey {keyIdx, secretKey} ->
|
||||
bbsPublicKey secretKey >>= \case
|
||||
Left e -> pure $ Left $ "issuer secret is not a valid key: " <> e
|
||||
Right pk -> pure $ case M.lookup keyIdx badgePublicKeys of
|
||||
Just pk' | pk' == pk -> Right k
|
||||
Just _ -> Left $ "issuer secret does not match the configured key at index " <> show keyIdx <> ", its public key is " <> T.unpack (safeDecodeUtf8 $ strEncode pk)
|
||||
Nothing -> Left $ "no configured badge key at index " <> show keyIdx <> ", clients could not verify what this service signs"
|
||||
|
||||
requireIssuerKey :: BadgeServiceOpts -> ChatConfig -> IO BadgeIssuerKey
|
||||
requireIssuerKey opts cfg =
|
||||
checkIssuerKey opts cfg >>= either (\e -> putStrLn ("Error: " <> e) >> exitFailure) pure
|
||||
|
||||
badgeService :: BadgeServiceOpts -> ChatConfig -> ServiceState -> IO ()
|
||||
badgeService opts cfg env = do
|
||||
key <- requireIssuerKey opts cfg
|
||||
let chatHooks =
|
||||
defaultChatHooks
|
||||
{ preStartHook = Just $ badgePreStartHook opts,
|
||||
postStartHook = Just $ badgePostStartHook opts env,
|
||||
preCmdHook = Just badgeCmdHook
|
||||
}
|
||||
-- the reader must not block: outputQ carries every chat event
|
||||
simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \_ cc ->
|
||||
raceAny_
|
||||
[ forever $
|
||||
atomically (readTBQueue $ outputQ cc) >>= \case
|
||||
(_, Right (CEvtServiceRequest u reqId sigKey reqData)) ->
|
||||
atomically $ writeTQueue (serviceRequestQ env) (u, reqId, sigKey, reqData)
|
||||
_ -> pure (),
|
||||
processQueuedRequests key env
|
||||
]
|
||||
|
||||
badgeServiceCLI :: BadgeServiceOpts -> IO ()
|
||||
badgeServiceCLI opts = do
|
||||
key <- requireIssuerKey opts terminalChatConfig
|
||||
env <- newServiceState
|
||||
let eventHook _cc ev = do
|
||||
case ev of
|
||||
Right (CEvtServiceRequest u reqId sigKey reqData) ->
|
||||
atomically $ writeTQueue (serviceRequestQ env) (u, reqId, sigKey, reqData)
|
||||
_ -> pure ()
|
||||
pure ev
|
||||
chatHooks =
|
||||
defaultChatHooks
|
||||
{ preStartHook = Just $ badgePreStartHook opts,
|
||||
postStartHook = Just $ badgePostStartHook opts env,
|
||||
preCmdHook = Just badgeCmdHook,
|
||||
eventHook = Just eventHook
|
||||
}
|
||||
raceAny_
|
||||
[ simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing,
|
||||
processQueuedRequests key env
|
||||
]
|
||||
|
||||
-- | issuing codes lives here rather than in core: every user's app would otherwise ship it
|
||||
badgeCmdHook :: ChatController -> ChatCommand -> IO (Either (Either ChatError ChatResponse) ChatCommand)
|
||||
badgeCmdHook cc = \case
|
||||
CustomChatCommand cmd -> Left <$> runBadgeCmd cc cmd
|
||||
cmd -> pure $ Right cmd
|
||||
|
||||
runBadgeCmd :: ChatController -> ByteString -> IO (Either ChatError ChatResponse)
|
||||
runBadgeCmd cc cmd = case A.parseOnly issueCmdP cmd of
|
||||
Left _ -> pure $ chatCmdError "use: //issue supporter|legend|investor [months 1-255] [paid|unpaid|free]"
|
||||
Right issueOpts ->
|
||||
issueBadgeCode cc issueOpts >>= \case
|
||||
Right code -> pure $ Right CRCustomChatResponse {user_ = Nothing, response = "code " <> formatBadgeCode code}
|
||||
Left e -> pure $ chatCmdError $ "issuing code: " <> e
|
||||
|
||||
issueCmdP :: A.Parser IssueCodeOpts
|
||||
issueCmdP =
|
||||
"issue " *> do
|
||||
badgeType <- badgeTypeP
|
||||
months_ <- optional (A.space *> A.decimal)
|
||||
-- outside `optional`, which would otherwise backtrack past a bad count
|
||||
months <- maybe (pure 1) checkMonths months_
|
||||
paymentStatus <- fromMaybe CPSFree <$> optional (A.space *> textTokenP)
|
||||
A.skipSpace
|
||||
A.endOfInput
|
||||
pure IssueCodeOpts {badgeType, months, paymentStatus}
|
||||
where
|
||||
checkMonths n
|
||||
| n >= 1 && n <= (255 :: Int) = pure n
|
||||
| otherwise = fail "months must be between 1 and 255"
|
||||
-- BadgeType decodes anything to BTUnknown, so a typo would issue an unusable code
|
||||
badgeTypeP =
|
||||
textTokenP >>= \case
|
||||
BTUnknown t -> fail $ "unknown badge type " <> T.unpack t
|
||||
bt -> pure bt
|
||||
textTokenP :: TextEncoding a => A.Parser a
|
||||
textTokenP = do
|
||||
t <- A.takeWhile1 (not . isSpace)
|
||||
maybe (fail "invalid value") pure $ textDecode $ safeDecodeUtf8 t
|
||||
|
||||
data IssueCodeOpts = IssueCodeOpts
|
||||
{ badgeType :: BadgeType,
|
||||
months :: Int,
|
||||
paymentStatus :: BadgeCodePaymentStatus
|
||||
}
|
||||
|
||||
-- | The caller sees the code once; only its hash is stored, so a lost code cannot be recovered.
|
||||
issueBadgeCode :: ChatController -> IssueCodeOpts -> IO (Either String BadgeCode)
|
||||
issueBadgeCode cc IssueCodeOpts {badgeType, months, paymentStatus} = do
|
||||
code <- randomBadgeCode $ random cc
|
||||
now <- getCurrentTime
|
||||
r <- withDB' "issueBadgeCode" cc $ \db -> insertBadgeCode db (badgeCodeHash code) badgeType months paymentStatus now
|
||||
pure $ code <$ r
|
||||
|
||||
processQueuedRequests :: BadgeIssuerKey -> ServiceState -> IO ()
|
||||
processQueuedRequests key env = do
|
||||
cc <- atomically $ readTMVar $ serviceCC env
|
||||
forever $ do
|
||||
(u, reqId, sigKey, reqData) <- atomically $ readTQueue $ serviceRequestQ env
|
||||
handleServiceRequest key cc u reqId sigKey reqData
|
||||
|
||||
badgePreStartHook :: BadgeServiceOpts -> ChatController -> IO ()
|
||||
badgePreStartHook opts ChatController {config, chatStore} =
|
||||
runBadgeServiceMigrations opts config chatStore
|
||||
|
||||
badgePostStartHook :: BadgeServiceOpts -> ServiceState -> ChatController -> IO ()
|
||||
badgePostStartHook BadgeServiceOpts {noAddress, testing} env cc = do
|
||||
-- SREQ delivery gates on this flag; Core starts serviceRequests=False, so the hook must set it.
|
||||
atomically $ writeTVar (processServiceRequests cc) True
|
||||
readTVarIO (currentUser cc) >>= \case
|
||||
Nothing -> putStrLn "No current user" >> exitFailure
|
||||
-- DR required for service RPC; autoAccept off because badge service ignores contact events.
|
||||
Just _ -> do
|
||||
unless noAddress $ initializeBotAddress' (not testing) (Just True) False cc
|
||||
void $ atomically $ tryPutTMVar (serviceCC env) cc
|
||||
|
||||
handleServiceRequest :: BadgeIssuerKey -> ChatController -> User -> AgentInvId -> Maybe C.PublicKeyEd25519 -> J.Object -> IO ()
|
||||
handleServiceRequest key cc User {userId} reqId sigKey reqData = do
|
||||
let reqIdT = safeDecodeUtf8 (strEncode reqId)
|
||||
logInfo $ "badge service request " <> reqIdT
|
||||
resp <- badgeServiceResponse key cc sigKey reqData
|
||||
sendChatCmd cc (APISendServiceResponse userId reqId (responseObject resp)) >>= \case
|
||||
Right _ -> pure ()
|
||||
Left e -> logError $ "badge service response failed for " <> reqIdT <> ": " <> tshow e
|
||||
|
||||
responseObject :: BadgeServiceResponse -> J.Object
|
||||
responseObject r = case J.toJSON r of
|
||||
J.Object o -> o
|
||||
_ -> KM.fromList [("type", J.String "error"), ("code", J.toJSON BSEInternal)]
|
||||
|
||||
errorResponse :: BadgeServiceErrorCode -> BadgeServiceResponse
|
||||
errorResponse code = BSPError {code, message = Nothing, retryAfter = badgeErrorRetryAfter code}
|
||||
|
||||
-- | Seconds, for the three codes badges-rpc.md marks transient. Every other code is terminal for
|
||||
-- the command attempted - internal included, which would otherwise press a failing service.
|
||||
badgeErrorRetryAfter :: BadgeServiceErrorCode -> Maybe Word32
|
||||
badgeErrorRetryAfter = \case
|
||||
BSEPaymentPending -> Just 300
|
||||
BSEProviderUnavailable -> Just 300
|
||||
BSERateLimited -> Just 60
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
-- | The agent verified the signature, so sigKey is a key the sender holds - a purchaseKey that
|
||||
-- differs would let a client claim a purchase it cannot sign for.
|
||||
badgeServiceResponse :: BadgeIssuerKey -> ChatController -> Maybe C.PublicKeyEd25519 -> J.Object -> IO BadgeServiceResponse
|
||||
badgeServiceResponse key cc sigKey reqData = case J.fromJSON (J.Object reqData) of
|
||||
J.Error _ -> pure $ errorResponse BSEBadRequest
|
||||
J.Success BadgeServiceRequest {version, purchaseKey, request}
|
||||
| not (version `isCompatible` supportedBadgeServiceVRange) -> pure $ errorResponse BSEUnsupportedVersion
|
||||
| purchaseKey /= sigKey -> pure $ errorResponse BSEBadRequest
|
||||
| otherwise -> case request of
|
||||
BSCRedeemBadgeCode {masterKey, code} -> case purchaseKey of
|
||||
Just k -> redeemCode key cc k masterKey code
|
||||
Nothing -> pure $ errorResponse BSEBadRequest
|
||||
BSCIssueBadge {balance} -> case purchaseKey of
|
||||
Just k -> issueBadgeCmd key cc k balance
|
||||
Nothing -> pure $ errorResponse BSEBadRequest
|
||||
-- every command but redeemBadgeCode needs a key the service already knows: that one
|
||||
-- creates the purchase, so its key is unknown on a first redemption
|
||||
_ -> case purchaseKey of
|
||||
Nothing -> pure $ errorResponse BSEUnsupportedVersion
|
||||
Just k ->
|
||||
withDB' "purchaseKeyExists" cc (`purchaseKeyExists` k) >>= \case
|
||||
Right True -> pure $ errorResponse BSEUnsupportedVersion
|
||||
Right False -> pure $ errorResponse BSEUnknownPurchaseKey
|
||||
Left _ -> pure $ errorResponse BSEInternal
|
||||
|
||||
-- | The only clock the service reads, so a test can move both sides of a request together.
|
||||
badgeNow :: ChatController -> IO UTCTime
|
||||
badgeNow ChatController {config = ChatConfig {badgeCurrentTime}} = badgeCurrentTime
|
||||
|
||||
randomId :: ChatController -> IO T.Text
|
||||
randomId cc = safeDecodeUtf8 . strEncode <$> atomically (C.randomBytes 16 (random cc))
|
||||
|
||||
-- | Neither value comes from the caller: the badge type is the entry's, the expiry is derived
|
||||
-- from the period end it carries.
|
||||
credentialForEntry :: BadgeIssuerKey -> BadgeMasterKey -> StatementEntry -> IO (Either String (StatementEntry, BadgeCredential))
|
||||
credentialForEntry BadgeIssuerKey {keyIdx, secretKey} masterKey e@StatementEntry {balanceStartTs = periodEnd, balanceBadgeType} = do
|
||||
let badgeInfo = BadgeInfo {badgeType = balanceBadgeType, badgeExpiry = endOfMondayAfter periodEnd, badgeExtra = ""}
|
||||
fmap (e,) <$> issueBadge keyIdx secretKey (VerifiedBadgeRequest BadgeRequest {masterKey, badgeInfo})
|
||||
|
||||
-- | Pairs the issued entry with the one before it, which the writer needs for the period start.
|
||||
issuanceAfter :: StatementEntry -> (StatementEntry, BadgeCredential) -> (StatementEntry, StatementEntry, BadgeCredential)
|
||||
issuanceAfter previous (issued, credential) = (previous, issued, credential)
|
||||
|
||||
credentialResponse :: Maybe BadgeCredential -> Maybe T.Text -> [StatementEntry] -> BadgeServiceResponse
|
||||
credentialResponse credential previousEntryId entries =
|
||||
BSPBadgeCredential {credential, receipt = Nothing, statement = BadgeStatement {entries, previousEntryId}}
|
||||
|
||||
-- | Nothing is written until the credential is signed, so a signing failure leaves the code
|
||||
-- unspent rather than spent with nothing behind it.
|
||||
redeemCode :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeMasterKey -> T.Text -> IO BadgeServiceResponse
|
||||
redeemCode key cc purchaseKey masterKey codeText = case parseBadgeCode codeText of
|
||||
Nothing -> pure $ errorResponse BSECodeInvalid
|
||||
Just code ->
|
||||
withDB "getBadgeCode" cc (readCode code) >>= \case
|
||||
Left _ -> pure $ errorResponse BSEInternal
|
||||
Right (Left resp) -> pure resp
|
||||
Right (Right IssuedCode {badgeCodeId, badgeType, months}) -> do
|
||||
now <- badgeNow cc
|
||||
(grantUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc
|
||||
-- the purchase is created here, so there is no ledger to lapse
|
||||
-- TODO [badges] a top-up grants onto an existing ledger, and must lapse before it or the
|
||||
-- months it adds are counted from a start already in the past
|
||||
let granted = grantEntry now grantUuid months SCCode $ emptyEntry now badgeType
|
||||
-- a grant of at least one month starting now always has a month to issue
|
||||
case issueEntry now issueUuid granted of
|
||||
Nothing -> pure $ errorResponse BSEInternal
|
||||
Just issued -> credentialForEntry key masterKey issued >>= \case
|
||||
Left e -> logError ("badge service signing failed: " <> T.pack e) $> errorResponse BSEInternal
|
||||
Right signed -> do
|
||||
-- re-read: a concurrent redemption may have landed while this one was signing
|
||||
r <- withDB "writeCodeRedemption" cc $ \db ->
|
||||
readCode code db >>= \case
|
||||
Left resp -> pure resp
|
||||
Right _ -> liftIO $ do
|
||||
purchaseId <- createCodePurchase db NewCodePurchase {badgeCodeId, purchaseKey, masterKey, badgeType} now
|
||||
appendLedgerPlan db purchaseId [granted] $ Just $ issuanceAfter granted signed
|
||||
entries_ <- getLedgerEntries db purchaseId 0
|
||||
pure $ maybe (errorResponse BSEInternal) (credentialResponse (Just $ snd signed) Nothing) entries_
|
||||
pure $ fromRight (errorResponse BSEInternal) r
|
||||
where
|
||||
-- used before signing and again inside the write transaction; every Left is a finished
|
||||
-- response, an unknown code included
|
||||
readCode code db = liftIO $
|
||||
getBadgeCode db (badgeCodeHash code) >>= \case
|
||||
Nothing -> pure $ Left $ errorResponse BSECodeInvalid
|
||||
Just c@IssuedCode {redemption} -> fmap (const c) <$> checkUnspent db redemption
|
||||
checkUnspent db = \case
|
||||
CodeUnredeemed -> pure $ Right ()
|
||||
CodeRedeemedUnreadable -> pure $ Left $ errorResponse BSEInternal
|
||||
CodeRedeemed RedeemedCode {purchaseKey = k, badgePurchaseId, credential}
|
||||
| k /= purchaseKey -> pure $ Left $ errorResponse BSECodeUsed
|
||||
-- the whole ledger, so a client that lost the first response still ends holding it
|
||||
| otherwise ->
|
||||
maybe (Left $ errorResponse BSEInternal) (Left . credentialResponse (Just credential) Nothing)
|
||||
<$> getLedgerEntries db badgePurchaseId 0
|
||||
|
||||
-- | The purchase is reached through the verified signer key and no other way.
|
||||
issueBadgeCmd :: BadgeIssuerKey -> ChatController -> C.PublicKeyEd25519 -> BadgeBalance -> IO BadgeServiceResponse
|
||||
issueBadgeCmd key cc purchaseKey BadgeBalance {lastEntry} = do
|
||||
now <- badgeNow cc
|
||||
purchase_ <- withDB' "getBadgePurchase" cc $ \db -> do
|
||||
p_ <- getPurchaseByKey db purchaseKey
|
||||
forM p_ $ \p@ServicePurchase {badgePurchaseId} -> (p,) <$> getLedgerTip db badgePurchaseId
|
||||
case purchase_ of
|
||||
Left _ -> pure $ errorResponse BSEInternal
|
||||
Right Nothing -> pure $ errorResponse BSEUnknownPurchaseKey
|
||||
Right (Just (ServicePurchase {badgePurchaseId, masterKey, badgeType}, tip)) -> do
|
||||
(lapseUuid, issueUuid) <- (,) <$> randomId cc <*> randomId cc
|
||||
let tipEntry = fromMaybe (emptyEntry now badgeType) tip
|
||||
lapsed = lapseEntry now lapseUuid tipEntry
|
||||
current = fromMaybe tipEntry lapsed
|
||||
case issueEntry now issueUuid current of
|
||||
Nothing -> writeIssued badgePurchaseId tip (maybeToList lapsed) now Nothing
|
||||
Just e ->
|
||||
credentialForEntry key masterKey e >>= \case
|
||||
Left err -> logError ("badge service signing failed: " <> T.pack err) $> errorResponse BSEInternal
|
||||
Right signed ->
|
||||
writeIssued badgePurchaseId tip (maybeToList lapsed) now $ Just $ issuanceAfter current signed
|
||||
where
|
||||
-- the rows were computed from a tip that another request may have moved, and an issuance was
|
||||
-- signed against it - so write only if it is still the tip
|
||||
writeIssued purchaseId tip rows t issuance_ = do
|
||||
r <- withDB "issueBadge" cc $ \db -> liftIO $ do
|
||||
tip' <- getLedgerTip db purchaseId
|
||||
when (fmap entryId tip' == fmap entryId tip) $ appendLedgerPlan db purchaseId rows issuance_
|
||||
issueResponse db purchaseId t
|
||||
pure $ fromRight (errorResponse BSEInternal) r
|
||||
-- entries after the one asserted, or the whole ledger when this purchase does not hold it.
|
||||
-- Only the asserted entry's identity is read, never the months it claims.
|
||||
issueResponse db purchaseId t = do
|
||||
let StatementEntry {entryId = assertedUuid} = lastEntry
|
||||
assertedId <- getLedgerEntryId db purchaseId assertedUuid
|
||||
-- TODO [badges] when the assertion does not resolve, heal the ledger and restate it as a
|
||||
-- single opening credit (badges-rpc.md), rather than resending the whole history
|
||||
entries_ <- getLedgerEntries db purchaseId (fromMaybe 0 assertedId)
|
||||
credential_ <- getCurrentIssuance db purchaseId t
|
||||
pure $ maybe (errorResponse BSEInternal) (credentialResponse credential_ (assertedUuid <$ assertedId)) entries_
|
||||
@@ -0,0 +1,256 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module BadgeService.Store
|
||||
( IssuedCode (..),
|
||||
CodeRedemption (..),
|
||||
RedeemedCode (..),
|
||||
NewCodePurchase (..),
|
||||
ServicePurchase (..),
|
||||
getBadgeCode,
|
||||
purchaseKeyExists,
|
||||
getPurchaseByKey,
|
||||
getLedgerTip,
|
||||
getLedgerEntryId,
|
||||
getLedgerEntries,
|
||||
getCurrentIssuance,
|
||||
appendLedgerPlan,
|
||||
createCodePurchase,
|
||||
insertBadgeCode,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Simplex.Chat.Badges (BadgeCredential, BadgeMasterKey (..), BadgeType)
|
||||
import Simplex.Chat.Badges.Ledger
|
||||
import Simplex.Chat.Badges.Service (StatementEntry (..))
|
||||
import Simplex.Chat.Badges.Types (BadgeCodePaymentStatus, BadgePurchaseStatus (..))
|
||||
import Simplex.Chat.Store.Shared (insertedRowId)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Util (maybeFirstRow, maybeFirstRow')
|
||||
|
||||
#if defined(dbPostgres)
|
||||
import Database.PostgreSQL.Simple (Only (..), (:.) (..))
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
#else
|
||||
import Database.SQLite.Simple (Only (..), (:.) (..))
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
#endif
|
||||
|
||||
data IssuedCode = IssuedCode
|
||||
{ badgeCodeId :: Int64,
|
||||
badgeType :: BadgeType,
|
||||
months :: Int,
|
||||
redemption :: CodeRedemption
|
||||
}
|
||||
|
||||
-- A code that has a purchase is spent, even if its credential cannot be read. Treating that as
|
||||
-- an unredeemed code would issue a second credential for it.
|
||||
data CodeRedemption
|
||||
= CodeUnredeemed
|
||||
| CodeRedeemed RedeemedCode
|
||||
| CodeRedeemedUnreadable
|
||||
|
||||
data RedeemedCode = RedeemedCode
|
||||
{ badgePurchaseId :: Int64,
|
||||
purchaseKey :: C.PublicKeyEd25519,
|
||||
credential :: BadgeCredential
|
||||
}
|
||||
|
||||
-- Its rows and issuance are appended by 'appendLedgerPlan' in the same transaction: a code marked
|
||||
-- redeemed while another write failed would be spent with no credential, and nothing reissues it.
|
||||
data NewCodePurchase = NewCodePurchase
|
||||
{ badgeCodeId :: Int64,
|
||||
purchaseKey :: C.PublicKeyEd25519,
|
||||
masterKey :: BadgeMasterKey,
|
||||
badgeType :: BadgeType
|
||||
}
|
||||
|
||||
data ServicePurchase = ServicePurchase
|
||||
{ badgePurchaseId :: Int64,
|
||||
masterKey :: BadgeMasterKey,
|
||||
badgeType :: BadgeType
|
||||
}
|
||||
|
||||
getBadgeCode :: DB.Connection -> ByteString -> IO (Maybe IssuedCode)
|
||||
getBadgeCode db codeHash =
|
||||
maybeFirstRow toCode $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT c.badge_code_id, c.badge_type, c.months, p.badge_purchase_id, p.purchase_key, i.credential
|
||||
FROM sx_badge_service_badge_codes c
|
||||
LEFT JOIN sx_badge_service_badge_purchases p ON p.badge_code_id = c.badge_code_id
|
||||
LEFT JOIN sx_badge_service_badge_issuances i ON i.badge_purchase_id = p.badge_purchase_id
|
||||
WHERE c.code_hash = ?
|
||||
ORDER BY i.period_end DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(Only (Binary codeHash))
|
||||
where
|
||||
toCode (badgeCodeId, badgeType, months, purchaseId_, purchaseKey_, credential_) =
|
||||
IssuedCode {badgeCodeId, badgeType, months, redemption = codeRedemption purchaseId_ purchaseKey_ credential_}
|
||||
codeRedemption purchaseId_ purchaseKey_ credential_ = case (purchaseId_, purchaseKey_) of
|
||||
(Just badgePurchaseId, Just purchaseKey) -> case decodeCredential =<< credential_ of
|
||||
Just credential -> CodeRedeemed RedeemedCode {badgePurchaseId, purchaseKey, credential}
|
||||
Nothing -> CodeRedeemedUnreadable
|
||||
_ -> CodeUnredeemed
|
||||
decodeCredential (Binary bs) = J.decodeStrict' bs
|
||||
|
||||
purchaseKeyExists :: DB.Connection -> C.PublicKeyEd25519 -> IO Bool
|
||||
purchaseKeyExists db key =
|
||||
maybeFirstRow' False (\(Only (_ :: Int64)) -> True) $
|
||||
DB.query db "SELECT badge_purchase_id FROM sx_badge_service_badge_purchases WHERE purchase_key = ?" (Only key)
|
||||
|
||||
-- | The only route from a command to a purchase, so a client cannot name one it cannot sign for.
|
||||
getPurchaseByKey :: DB.Connection -> C.PublicKeyEd25519 -> IO (Maybe ServicePurchase)
|
||||
getPurchaseByKey db key =
|
||||
maybeFirstRow toPurchase $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT badge_purchase_id, master_key, current_badge_type
|
||||
FROM sx_badge_service_badge_purchases
|
||||
WHERE purchase_key = ?
|
||||
|]
|
||||
(Only key)
|
||||
where
|
||||
toPurchase (badgePurchaseId, Binary mk, badgeType) =
|
||||
ServicePurchase {badgePurchaseId, masterKey = BadgeMasterKey mk, badgeType}
|
||||
|
||||
getLedgerTip :: DB.Connection -> Int64 -> IO (Maybe StatementEntry)
|
||||
getLedgerTip db purchaseId =
|
||||
maybeFirstRow' Nothing toEntry $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT entry_uuid, change_months, balance_months, balance_start_ts, balance_anchor_ts, balance_badge_type,
|
||||
entry_type, entry_credit_type, entry_debit_type, service_created_at
|
||||
FROM sx_badge_service_badge_ledger
|
||||
WHERE badge_purchase_id = ?
|
||||
ORDER BY entry_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(Only purchaseId)
|
||||
|
||||
-- | The uuid is the client's claim about its last held entry, so the lookup is scoped to its own
|
||||
-- purchase - an entry_id taken from another ledger would silently skip rows of this one.
|
||||
getLedgerEntryId :: DB.Connection -> Int64 -> Text -> IO (Maybe Int64)
|
||||
getLedgerEntryId db purchaseId entryUuid =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT entry_id FROM sx_badge_service_badge_ledger WHERE badge_purchase_id = ? AND entry_uuid = ?"
|
||||
(purchaseId, entryUuid)
|
||||
|
||||
-- | 0 for the whole ledger, as entry_id starts at 1. 'Nothing' when a stored row has a type this
|
||||
-- version cannot represent, rather than sending it changed into another.
|
||||
getLedgerEntries :: DB.Connection -> Int64 -> Int64 -> IO (Maybe [StatementEntry])
|
||||
getLedgerEntries db purchaseId afterEntryId =
|
||||
mapM toEntry
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT entry_uuid, change_months, balance_months, balance_start_ts, balance_anchor_ts, balance_badge_type,
|
||||
entry_type, entry_credit_type, entry_debit_type, service_created_at
|
||||
FROM sx_badge_service_badge_ledger
|
||||
WHERE badge_purchase_id = ? AND entry_id > ?
|
||||
ORDER BY entry_id
|
||||
|]
|
||||
(purchaseId, afterEntryId)
|
||||
|
||||
toEntry :: (Text, Int, Int, UTCTime, UTCTime, BadgeType, Text, Maybe Text, Maybe Text, UTCTime) -> Maybe StatementEntry
|
||||
toEntry (entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType, entryType_, credit_, debit_, createdAt) =
|
||||
(\entryType -> StatementEntry {entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType, wasPausedSince = Nothing, createdAt, entryType})
|
||||
<$> entryTypeFromColumns entryType_ credit_ debit_
|
||||
|
||||
-- | Answers a repeat inside an issued month, rather than signing the same content twice.
|
||||
getCurrentIssuance :: DB.Connection -> Int64 -> UTCTime -> IO (Maybe BadgeCredential)
|
||||
getCurrentIssuance db purchaseId now = do
|
||||
rs <-
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT credential FROM sx_badge_service_badge_issuances
|
||||
WHERE badge_purchase_id = ? AND period_end > ?
|
||||
ORDER BY period_end DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(purchaseId, now)
|
||||
pure $ case rs of
|
||||
[Only (Binary bs)] -> J.decodeStrict' bs
|
||||
_ -> Nothing
|
||||
|
||||
-- | The issuance is the entry that spends the month and the one before it, which give the period.
|
||||
-- TODO [badges] also write the reference columns - payment_id, charge_id, from_purchase_id,
|
||||
-- to_purchase_id - for the entry types that carry one. Only the tag is written today, so a
|
||||
-- payment, charge, transferIn, upgrade or transferOut row would be stored without its reference.
|
||||
appendLedgerPlan :: DB.Connection -> Int64 -> [StatementEntry] -> Maybe (StatementEntry, StatementEntry, BadgeCredential) -> IO ()
|
||||
appendLedgerPlan db purchaseId rows issuance_ = do
|
||||
mapM_ appendRow rows
|
||||
case issuance_ of
|
||||
Nothing -> pure ()
|
||||
Just (previous, issued@StatementEntry {entryId, balanceStartTs = periodEnd, balanceBadgeType, createdAt}, credential) -> do
|
||||
rowId <- appendRow issued
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO sx_badge_service_badge_issuances
|
||||
(issuance_id, badge_purchase_id, entry_id, badge_type, period_start, period_end, expiry, credential, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
-- the issued entry's uuid is the issuance id: one issuance per such entry, and entry uuids
|
||||
-- are already unique across the ledger, so nothing has to be drawn for it
|
||||
( (entryId, purchaseId, rowId, balanceBadgeType)
|
||||
:. (balanceStartTs previous, periodEnd, endOfMondayAfter periodEnd, Binary (LB.toStrict $ J.encode credential), createdAt)
|
||||
)
|
||||
where
|
||||
appendRow StatementEntry {entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType, createdAt, entryType} = do
|
||||
let (entryTypeT, creditType, debitType) = entryTypeColumns entryType
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO sx_badge_service_badge_ledger
|
||||
(entry_uuid, badge_purchase_id, change_months, balance_months, balance_start_ts, balance_anchor_ts,
|
||||
balance_badge_type, service_created_at, created_at, entry_type, entry_credit_type, entry_debit_type)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
((entryId, purchaseId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs) :. (balanceBadgeType, createdAt, createdAt, entryTypeT, creditType, debitType))
|
||||
insertedRowId db
|
||||
|
||||
-- redeemed_at is stamped here, so this must share a transaction with the credential's rows:
|
||||
-- a code marked spent without one can never be reissued
|
||||
createCodePurchase :: DB.Connection -> NewCodePurchase -> UTCTime -> IO Int64
|
||||
createCodePurchase db NewCodePurchase {badgeCodeId, purchaseKey, masterKey = BadgeMasterKey mk, badgeType} now = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO sx_badge_service_badge_purchases
|
||||
(purchase_key, master_key, initial_badge_type, current_badge_type, status, badge_code_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(purchaseKey, Binary mk, badgeType, badgeType, PSIssued, badgeCodeId, now, now)
|
||||
purchaseId <- insertedRowId db
|
||||
DB.execute db "UPDATE sx_badge_service_badge_codes SET redeemed_at = ? WHERE badge_code_id = ?" (now, badgeCodeId)
|
||||
pure purchaseId
|
||||
|
||||
insertBadgeCode :: DB.Connection -> ByteString -> BadgeType -> Int -> BadgeCodePaymentStatus -> UTCTime -> IO ()
|
||||
insertBadgeCode db codeHash badgeType months paymentStatus now =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO sx_badge_service_badge_codes (code_hash, badge_type, months, code_payment_status, created_at)
|
||||
VALUES (?,?,?,?,?)
|
||||
|]
|
||||
(Binary codeHash, badgeType, months, paymentStatus, now)
|
||||
@@ -0,0 +1,40 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module BadgeService.Store.Migrate
|
||||
( runBadgeServiceMigrations,
|
||||
)
|
||||
where
|
||||
|
||||
import BadgeService.Options
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Options (CoreChatOpts (..))
|
||||
import Simplex.Chat.Options.DB
|
||||
import Simplex.Messaging.Agent.Store.Common
|
||||
import Simplex.Messaging.Agent.Store.Interface (migrateDBSchema)
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..))
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
#if defined(dbPostgres)
|
||||
import BadgeService.Store.Postgres.Migrations
|
||||
#else
|
||||
import BadgeService.Store.SQLite.Migrations
|
||||
#endif
|
||||
|
||||
runBadgeServiceMigrations :: BadgeServiceOpts -> ChatConfig -> DBStore -> IO ()
|
||||
runBadgeServiceMigrations opts ChatConfig {confirmMigrations} chatStore =
|
||||
migrateDBSchema
|
||||
chatStore
|
||||
(toDBOpts dbOptions chatSuffix False [])
|
||||
(Just "sx_badge_service_migrations")
|
||||
badgeServiceSchemaMigrations
|
||||
MigrationConfig {confirm, backupPath = Nothing}
|
||||
>>= either (exit . ("badge service migrations " <>) . show) pure
|
||||
where
|
||||
BadgeServiceOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations}} = opts
|
||||
confirm = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
|
||||
|
||||
exit :: String -> IO a
|
||||
exit err = putStrLn ("Error: " <> err) >> exitFailure
|
||||
@@ -0,0 +1,85 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module BadgeService.Store.Postgres.Migrations (badgeServiceSchemaMigrations) where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges (badgeSchema, badgeSchemaDown, withPrefix)
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
badgeServiceSchemaMigrations :: [Migration]
|
||||
badgeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up, down}
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
schemaMigrations =
|
||||
[ ("20260806_badge_service_schema", m20260806_badge_service_schema, Just down_m20260806_badge_service_schema)
|
||||
]
|
||||
|
||||
-- the client tables are in the same database, so the service tables are the same names with this prefix
|
||||
servicePrefix :: Text
|
||||
servicePrefix = "sx_badge_service_"
|
||||
|
||||
m20260806_badge_service_schema :: Text
|
||||
m20260806_badge_service_schema =
|
||||
badgeSchema servicePrefix
|
||||
<> withPrefix
|
||||
servicePrefix
|
||||
[r|
|
||||
CREATE TABLE @badge_codes(
|
||||
badge_code_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
code_hash BYTEA NOT NULL,
|
||||
badge_type TEXT NOT NULL,
|
||||
months SMALLINT NOT NULL,
|
||||
code_payment_status TEXT NOT NULL,
|
||||
redeemed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
UNIQUE(code_hash)
|
||||
);
|
||||
|
||||
ALTER TABLE @badge_purchases ADD COLUMN badge_code_id BIGINT REFERENCES @badge_codes;
|
||||
|
||||
CREATE UNIQUE INDEX @idx_badge_purchases_code ON @badge_purchases(badge_code_id);
|
||||
|
||||
CREATE TABLE @badge_code_invoices(
|
||||
invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES @invoices ON DELETE CASCADE,
|
||||
price_id TEXT NOT NULL REFERENCES @badge_prices,
|
||||
offer_id TEXT REFERENCES @badge_offers,
|
||||
months SMALLINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX @idx_badge_code_invoices_offer ON @badge_code_invoices(offer_id);
|
||||
|
||||
CREATE INDEX @idx_badge_code_invoices_price ON @badge_code_invoices(price_id);
|
||||
|]
|
||||
|
||||
down_m20260806_badge_service_schema :: Text
|
||||
down_m20260806_badge_service_schema =
|
||||
withPrefix
|
||||
servicePrefix
|
||||
[r|
|
||||
DROP INDEX @idx_badge_code_invoices_offer;
|
||||
DROP INDEX @idx_badge_code_invoices_price;
|
||||
DROP TABLE @badge_code_invoices;
|
||||
|
||||
DROP INDEX @idx_badge_purchases_code;
|
||||
|]
|
||||
<> badgeSchemaDown servicePrefix
|
||||
<> withPrefix
|
||||
servicePrefix
|
||||
[r|
|
||||
DROP TABLE @badge_codes;
|
||||
|]
|
||||
|
||||
{- TODO [badges] deferred with the draft in M20260915_user_badges, service only.
|
||||
|
||||
ALTER TABLE @payments ADD COLUMN receipt_hash BYTEA;
|
||||
|
||||
-- down
|
||||
ALTER TABLE @payments DROP COLUMN receipt_hash;
|
||||
-}
|
||||
@@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module BadgeService.Store.SQLite.Migrations (badgeServiceSchemaMigrations) where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Data.Text (Text)
|
||||
import Database.SQLite.Simple (Query (..))
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges (badgeSchema, badgeSchemaDown, withPrefix)
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
badgeServiceSchemaMigrations :: [Migration]
|
||||
badgeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down}
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
schemaMigrations =
|
||||
[ ("20260806_badge_service_schema", m20260806_badge_service_schema, Just down_m20260806_badge_service_schema)
|
||||
]
|
||||
|
||||
-- the client tables are in the same database, so the service tables are the same names with this prefix
|
||||
servicePrefix :: Text
|
||||
servicePrefix = "sx_badge_service_"
|
||||
|
||||
m20260806_badge_service_schema :: Query
|
||||
m20260806_badge_service_schema =
|
||||
badgeSchema servicePrefix
|
||||
<> withPrefix
|
||||
servicePrefix
|
||||
[sql|
|
||||
CREATE TABLE @badge_codes(
|
||||
badge_code_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code_hash BLOB NOT NULL,
|
||||
badge_type TEXT NOT NULL,
|
||||
months INTEGER NOT NULL,
|
||||
code_payment_status TEXT NOT NULL,
|
||||
redeemed_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(code_hash)
|
||||
) STRICT;
|
||||
|
||||
ALTER TABLE @badge_purchases ADD COLUMN badge_code_id INTEGER REFERENCES @badge_codes;
|
||||
|
||||
CREATE UNIQUE INDEX @idx_badge_purchases_code ON @badge_purchases(badge_code_id);
|
||||
|
||||
CREATE TABLE @badge_code_invoices(
|
||||
invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES @invoices ON DELETE CASCADE,
|
||||
price_id TEXT NOT NULL REFERENCES @badge_prices,
|
||||
offer_id TEXT REFERENCES @badge_offers,
|
||||
months INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX @idx_badge_code_invoices_offer ON @badge_code_invoices(offer_id);
|
||||
|
||||
CREATE INDEX @idx_badge_code_invoices_price ON @badge_code_invoices(price_id);
|
||||
|]
|
||||
|
||||
down_m20260806_badge_service_schema :: Query
|
||||
down_m20260806_badge_service_schema =
|
||||
withPrefix
|
||||
servicePrefix
|
||||
[sql|
|
||||
DROP INDEX @idx_badge_code_invoices_offer;
|
||||
DROP INDEX @idx_badge_code_invoices_price;
|
||||
DROP TABLE @badge_code_invoices;
|
||||
|
||||
DROP INDEX @idx_badge_purchases_code;
|
||||
|]
|
||||
<> badgeSchemaDown servicePrefix
|
||||
<> withPrefix
|
||||
servicePrefix
|
||||
[sql|
|
||||
DROP TABLE @badge_codes;
|
||||
|]
|
||||
|
||||
{- TODO [badges] deferred with the draft in M20260915_user_badges, service only.
|
||||
|
||||
ALTER TABLE @payments ADD COLUMN receipt_hash BLOB;
|
||||
|
||||
-- down
|
||||
ALTER TABLE @payments DROP COLUMN receipt_hash;
|
||||
-}
|
||||
@@ -47,9 +47,9 @@ import Directory.Options
|
||||
import Directory.Search
|
||||
import Directory.Store
|
||||
import Directory.Store.Migrate
|
||||
import Directory.Util
|
||||
import Simplex.Chat.Bot
|
||||
import Simplex.Chat.Bot.KnownContacts
|
||||
import Simplex.Chat.Bot.Store
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Library.Internal (setGroupLinkData)
|
||||
@@ -216,7 +216,7 @@ directoryPostStartHook opts@DirectoryOpts {noAddress, testing} env cc =
|
||||
readTVarIO (currentUser cc) >>= \case
|
||||
Nothing -> putStrLn "No current user" >> exitFailure
|
||||
Just User {userId, profile = p@LocalProfile {preferences}} -> do
|
||||
unless noAddress $ initializeBotAddress' (not testing) cc
|
||||
unless noAddress $ initializeBotAddress' (not testing) Nothing True cc
|
||||
void $ atomically $ tryPutTMVar (serviceCC env) cc
|
||||
listingsUpdated env
|
||||
let cmds = fromMaybe [] $ preferences >>= commands_
|
||||
|
||||
@@ -68,7 +68,7 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Directory.Search
|
||||
import Directory.Util
|
||||
import Simplex.Chat.Bot.Store
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Names (claimDomain)
|
||||
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
|
||||
|
||||
@@ -342,6 +342,7 @@ undocumentedCommands =
|
||||
[ "APIAbortSwitchContact",
|
||||
"APIAbortSwitchGroupMember",
|
||||
"APIAcceptConditions",
|
||||
"APIAckBadgeAlert",
|
||||
"APIActivateChat",
|
||||
"APIAddGroupShortLink",
|
||||
"APIAddMyAddressShortLink",
|
||||
@@ -377,6 +378,7 @@ undocumentedCommands =
|
||||
"APIExportArchive",
|
||||
"APIForwardChatItems",
|
||||
"APIGetAppSettings",
|
||||
"APIGetBadgeState",
|
||||
"APIGetCallInvitations",
|
||||
"APIGetChat",
|
||||
"APIGetChatContentTypes",
|
||||
@@ -404,6 +406,7 @@ undocumentedCommands =
|
||||
"APIPlanForwardChatItems",
|
||||
"APIPrepareContact",
|
||||
"APIPrepareGroup",
|
||||
"APIRedeemBadgeCode",
|
||||
"APIRegisterToken",
|
||||
"APIRejectCall",
|
||||
"APIReorderChatTags",
|
||||
|
||||
@@ -179,6 +179,8 @@ undocumentedEvents =
|
||||
"CEvtAgentConnsDeleted",
|
||||
"CEvtAgentRcvQueuesDeleted",
|
||||
"CEvtAgentUserDeleted",
|
||||
"CEvtBadgeAlert",
|
||||
"CEvtBadgeChanged",
|
||||
"CEvtBusinessRequestAlreadyAccepted",
|
||||
"CEvtCallAnswer",
|
||||
"CEvtCallEnded",
|
||||
|
||||
@@ -134,6 +134,8 @@ undocumentedResponses =
|
||||
"CRAppSettings",
|
||||
"CRArchiveExported",
|
||||
"CRArchiveImported",
|
||||
"CRBadgeRedeemed",
|
||||
"CRBadgeState",
|
||||
"CRBroadcastSent",
|
||||
"CRCallInvitations",
|
||||
"CRChatCleared",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Badge service RPC protocol
|
||||
|
||||
Schema: `badges-rpc.schema.json`, definitions `request` and `response`. Types: `Simplex.Chat.Badges.Service`. Model: `plans/2026-07-30-supporter-badges-v3-ux.md` §3 — cited below as "model".
|
||||
|
||||
## Transport
|
||||
|
||||
Service RPC (`plans/2026-07-22-service-rpc-chat.md`, branch `rpc`): the request travels in `APISendServiceRequest.request`, the response in `CRServiceResponse.responseData`; one response per request; per-call timeout.
|
||||
|
||||
A request is an envelope: `version` — the client's protocol version; `purchaseKey`; `request` — the command, discriminated on `type`. Responses are discriminated on `type`. The service is deployed ahead of app releases, answers within the client's `version`, and rejects clients older than it supports with `unsupported_version`.
|
||||
|
||||
## Identity
|
||||
|
||||
Each purchase runs under a fresh Ed25519 key pair; `purchaseKey` is its public part and identifies the badge. The service cannot link purchases of one user; the exceptions are the declared upgrades below. `getBadgeCatalog` may omit `purchaseKey`: unsigned, it returns the catalog alone; signed, its response adds the purchase's `badgeStatement` — a client holding a lapsed badge checks for credits in the same request that prices a new purchase, and buys under a fresh key only when the statement shows none. Every other command requires the key and is signed with it. The agent delivers the verified signer key alongside the request; the service rejects a `purchaseKey` that differs from it with `bad_request`.
|
||||
|
||||
A purchase record is created by `redeemBadgeCode`, by `getBadgeInvoice`, or by `purchaseBadge` funded with `apple`, `google`, or `receipt`. Those commands accept a key the service holds no record of — on a first purchase it always will. Every other command answers `unknown_purchase_key` for such a key.
|
||||
|
||||
## Idempotency
|
||||
|
||||
A timeout hides the outcome, so the client repeats the identical signed request at its next trigger, never on a poll timer.
|
||||
|
||||
- `getBadgeInvoice` — returns the open invoice again; a new invoice is created only when none is open.
|
||||
- `redeemBadgeCode` — a code already redeemed by the signing key returns the same `badgeCredential` and writes nothing; redeemed by another key, `code_used`. The client must therefore keep the key it first signed with, or a retry cannot be recognised.
|
||||
- `purchaseBadge` — a payment already credited returns the same `badgeCredential` and writes nothing.
|
||||
- `upgradeBadgeSubscription` — evidence already applied returns the same result and writes nothing.
|
||||
- `issueBadge` — repeated within an issued period, returns the cached credential and writes nothing.
|
||||
- `purchaseBadge` with a `receipt` — presented again by the same key it returns the same result; presented by another key, `receipt_used`.
|
||||
|
||||
## Commands
|
||||
|
||||
`purchaseBadge` and `upgradeBadgeSubscription` carry `badgeRequest`, the signer's input (`BadgeRequest`, `Simplex.Chat.Badges`): the service signs exactly this content or rejects the command. The proposed `badgeExpiry` is capped by the funded coverage (`sundayAfter`, model §3) and is required — a credential always expires, and a badge that does not is expressed as a long finite term; `badgeExtra` is reserved and must be empty. `issueBadge` carries no `badgeRequest`: the tier, the master key and the expiry are all the service's own, so there is nothing for the client to state.
|
||||
|
||||
- `getBadgeCatalog` → `badgeCatalog` — the prices and offers; signed, also the purchase's `badgeStatement`. Store builds never send it: prices come from the store and SKUs from app config.
|
||||
- `getBadgeInvoice` → `badgeInvoice` — prices the purchase for `badgeInfo` and `paymentVia` (`card` — Stripe; `crypto` — btc, xmr). The response holds the generic `invoice` — `invoiceId`, `price`, `discount`, the upgrade `credit`, `amount` = price − discount − credit, `currency`, `expiresAt`, and `paymentTo` (`url` for card; `address` and `cryptoAmount` for crypto) — beside the badge part, `badgeType` and `months`. `priceId` pins the price the client displayed; `offerId` selects a discounted duration, and its absence buys one month at that price. Price and offer status is checked here only: `deprecated` is still accepted, `disabled` is rejected; a badge type with no active price yields `product_unavailable`.
|
||||
- `redeemBadgeCode` → `badgeCredential` — redeems a code, records the credit, and issues the first credential, in one round trip. It carries `masterKey` and `code` and no `badgeRequest`: a code states no tier and no expiry, so the credential is what reports them. Errors: `code_invalid` for an unknown or malformed code, `code_used` when another key redeemed it, `code_expired` past a redemption deadline.
|
||||
- `purchaseBadge` → `badgeCredential` — verifies the funding (`apple` JWS offline; `google` token via the Publisher API; `invoice` against webhook-confirmed settlement, `payment_pending` until it lands; `receipt`), records the credit, and issues the first credential, in one round trip. The response `receipt` is the recovery bearer secret (model § recovery); the service stores its hash; lifetime badges receive none.
|
||||
- Funding by `receipt` is a transfer (post-MVP): the unissued months of the purchase that receipt belongs to move to the signing key, recorded as `debit(transferOut)` on the source and `credit(transferIn)` on the new purchase, and the presented receipt is retired for a fresh one. The transferred period's issuance debits a month like any other. Lifetime badges hold no receipt, so support handles them.
|
||||
- `upgradeBadgeSubscription` → `badgeCredential` — the app-led store subscription change, on the same key: verifies the store evidence of the replaced subscription and records the new plan; an immediate upgrade returns the new credential, a deferred change returns none.
|
||||
- `issueBadge` → `badgeCredential` — issues the next period from the balance, the only source of issuance. It carries `balance` alone: the credential is signed with the purchase's stored master key, for the type the balance funds, expiring at the `sundayAfter` of the period issued. The ledger is advanced first; the credential is signed before the `debit(badge)` and issuance rows are written, in one transaction. An exhausted balance yields no `credential`; the `statement` shows why. Issuing on a paused badge resumes it (model 2.13).
|
||||
- `pauseBadge` (post-MVP) → `badgeCredential` — suspends issuance and lapse (model 2.13).
|
||||
|
||||
## Upgrades
|
||||
|
||||
Always a new purchase under a new key, except store subscriptions, where the store owns the change.
|
||||
|
||||
- Non-store: `getBadgeInvoice.upgrade` — `fromPurchaseKey`, the old purchase's `receipt`, `receiptSignature` binding the old key to the new, and the asserted old `balance`. The invoice returns the conversion `credit`; settlement records `debit(upgrade)` on the old purchase and the credit on the new.
|
||||
- Store one-time: an upgrade SKU at a fixed discounted price; `purchaseBadge.upgrade` — `fromPurchaseKey`, `receipt`, `receiptSignature` — proves eligibility (an unexpired cheaper badge), because the store cannot gate who buys the SKU.
|
||||
- Store subscription, app-led: the native subscription-group flow (Apple — immediate, with the store's prorated refund; Google — per replacement mode), then `upgradeBadgeSubscription` with the new evidence.
|
||||
- Store subscription, sheet-led, and every downgrade: the client sends nothing — the service discovers the change from provider state and notifications, and each renewal credits months of the charged badge type.
|
||||
|
||||
## Catalog
|
||||
|
||||
`prices` — `priceId`, `badgeType`, `monthPrice`, `currency`, `status`, `createdAt`; `offers` — `offerId`, `priceId`? (absent applies to any price), `months`, `discount`, `status`, `createdAt`. An offer states a discount, as free months or a percentage; a duration without one is priced at `months × monthPrice`. Repricing appends a price and deprecates the old, which is still accepted at invoice creation; deprecated prices and offers are sent so that a refresh cannot remove what the client pinned, and disabled ones are omitted. Rendering is app-driven — tiers and durations come from app resources, and one without a price is shown disabled.
|
||||
|
||||
## Statement and balance
|
||||
|
||||
The ledger is written by the service alone (model §3); the client keeps a verbatim replica and computes the effective balance from its last entry and the time. Month boundaries are counted from `balanceAnchorTs`, the start of the current run of months, and not from `balanceStartTs` — counting from the moving start would compound the day-of-month clipping of a short month, so a run beginning 31 January would reach 28 February and never return to the 31st. The service sets a new anchor only where a lapsed run restarts; months granted while coverage still runs extend it on its existing anchor.
|
||||
|
||||
`statement` — `entries`, and `previousEntryId` when they attach after an entry the client holds; its absence marks entries that attach to nothing. Each entry states `entryId`, the signed `changeMonths`, the resulting `balanceMonths`, `balanceStartTs`, `balanceAnchorTs`, and `balanceBadgeType`, `wasPausedSince` on the entry ending a pause, `createdAt`, and `entryType` — `credit`: `payment {invoiceId?}`, `code`, `charge {chargeId}`, `support`, `transferIn {fromPurchaseKey}`, `opening`; `debit`: `refund`, `upgrade {toPurchaseKey}`, `transferOut {toPurchaseKey}`, `support`, `badge`, `lapse`. A code grant is `code` rather than `payment` with no `invoiceId`: the invoice of a code belongs to whoever bought it, and the redeemer's ledger must never reference it. An unknown type is stored as received and decoded after an app upgrade.
|
||||
|
||||
`balance` — `lastEntry`, the client's last entry, asserting the position and the months it believes it holds.
|
||||
|
||||
An assertion that names an entry the service holds is a prefix: the service proceeds and returns what follows it. Otherwise the service heals its own ledger first — provider evidence for charges, time for lapses — proceeds, and returns either the complete history or one `opening` credit. An `opening` entry is an absolute restatement: the ledger is reset to the amount it states, without relation to the preceding entry, which also serves a new device and, later, the discarding of old history into a brought-forward balance.
|
||||
|
||||
## Errors
|
||||
|
||||
`retryAfter` marks the transient codes: `payment_pending`, `provider_unavailable`, `rate_limited`. `offer_disabled` calls for a catalog refresh. `code_invalid` covers unknown, malformed and revoked codes alike, so a guesser learns nothing from the difference; `code_used` — redeemed under another key; `code_expired` — past its redemption deadline. `receipt_invalid` covers unknown receipts. All other codes are terminal for the attempted command.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user