mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab5e7d02f | |||
| 29cc2cfb0c | |||
| 4826da0825 | |||
| 4f9f2cecec | |||
| a5c1be3b8f | |||
| f7cac769df | |||
| a4b086ce7a | |||
| 2a20afeda6 | |||
| beefb1ba12 | |||
| 6c1a821304 | |||
| ec40c2d7e5 | |||
| d6666b270f | |||
| 950b87b48f | |||
| ebbafc12d4 | |||
| 8053d8fc39 | |||
| d085a047fc | |||
| 99d5a183c4 | |||
| 8979306e68 | |||
| e5a60b963e |
@@ -5,22 +5,12 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- users
|
||||
tags:
|
||||
- "v*"
|
||||
- "!*-fdroid"
|
||||
- "!*-armv7a"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "apps/ios"
|
||||
- "apps/multiplatform"
|
||||
- "blog"
|
||||
- "docs"
|
||||
- "fastlane"
|
||||
- "images"
|
||||
- "packages"
|
||||
- "website"
|
||||
- "README.md"
|
||||
- "PRIVACY.md"
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
|
||||
@@ -300,27 +300,25 @@ What is already implemented:
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
|
||||
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
|
||||
4. [Post-quantum resistant key exchange](./docs/GLOSSARY.md#post-quantum-cryptography) in double ratchet protocol *on every ratchet step*. Read more in [this post](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) and also see this [publication by Apple]( https://security.apple.com/blog/imessage-pq3/) explaining the need for post-quantum key rotation.
|
||||
5. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
|
||||
6. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
|
||||
7. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
|
||||
8. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
9. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
10. To protect your IP address from unknown messaging relays, and for per-message transport anonymity (compared with Tor/VPN per-connection anonymity), from v6.0 all SimpleX Chat clients use private message routing by default. Read more in [this post](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing).
|
||||
11. To protect your IP address from unknown file relays, when SOCKS proxy is not enabled SimpleX Chat clients ask for a confirmation before downloading the files from unknown servers.
|
||||
12. To protect your IP address from known servers all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
|
||||
13. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
|
||||
14. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||
15. Manual messaging queue rotations to move conversation to another SMP relay.
|
||||
16. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||
17. Local files encryption.
|
||||
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
|
||||
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
|
||||
6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
|
||||
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
|
||||
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
|
||||
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
||||
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||
14. Local files encryption.
|
||||
|
||||
We plan to add:
|
||||
|
||||
1. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
2. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
3. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
|
||||
4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
|
||||
1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
|
||||
2. Post-quantum resistant key exchange in double ratchet protocol.
|
||||
3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
5. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
|
||||
|
||||
## For developers
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
},
|
||||
"symbols": [
|
||||
{
|
||||
"filename": "checkmark.2.svg",
|
||||
"idiom": "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>checkmark.2</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="double.checkmark">
|
||||
<g id="Notes">
|
||||
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
|
||||
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<text id="Weight/Scale-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="322">Weight/Scale Variations</tspan>
|
||||
</text>
|
||||
<text id="Ultralight" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="533.711" y="322">Ultralight</tspan>
|
||||
</text>
|
||||
<text id="Thin" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="843.422" y="322">Thin</tspan>
|
||||
</text>
|
||||
<text id="Light" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1138.63" y="322">Light</tspan>
|
||||
</text>
|
||||
<text id="Regular" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1426.84" y="322">Regular</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1723.06" y="322">Medium</tspan>
|
||||
</text>
|
||||
<text id="Semibold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2015.77" y="322">Semibold</tspan>
|
||||
</text>
|
||||
<text id="Bold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2326.48" y="322">Bold</tspan>
|
||||
</text>
|
||||
<text id="Heavy" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2618.19" y="322">Heavy</tspan>
|
||||
</text>
|
||||
<text id="Black" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2917.4" y="322">Black</tspan>
|
||||
</text>
|
||||
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<g id="Group" transform="translate(264.3672, 1918.0684)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M7.88088,15.76172 C12.18752,15.76172 15.7715,12.1875 15.7715,7.88086 C15.7715,3.57422 12.17774,0 7.8711,0 C3.57422,0 0,3.57422 0,7.88086 C0,12.1875 3.58398,15.76172 7.88086,15.76172 L7.88088,15.76172 Z M7.88088,14.277344 C4.33596,14.277344 1.50392,11.435544 1.50392,7.880864 C1.50392,4.326184 4.32618,1.484384 7.8711,1.484384 C11.42578,1.484384 14.27734,4.326184 14.27734,7.880864 C14.27734,11.435544 11.43554,14.277344 7.88086,14.277344 L7.88088,14.277344 Z M4.28712,7.880864 C4.28712,8.310552 4.589854,8.60352 5.039074,8.60352 L7.138674,8.60352 L7.138674,10.72266 C7.138674,11.162114 7.431642,11.464848 7.86133,11.464848 C8.310548,11.464848 8.603518,11.162114 8.603518,10.72266 L8.603518,8.60352 L10.722658,8.60352 C11.162112,8.60352 11.464846,8.310552 11.464846,7.880864 C11.464846,7.44141 11.162112,7.138676 10.722658,7.138676 L8.603518,7.138676 L8.603518,5.029296 C8.603518,4.580078 8.31055,4.277342 7.86133,4.277342 C7.431642,4.277342 7.138674,4.580076 7.138674,5.029296 L7.138674,7.138676 L5.039074,7.138676 C4.589856,7.138676 4.28712,7.44141 4.28712,7.880864 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(283.254, 1915.9883)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M9.96094,19.92188 C15.41016,19.92188 19.92188,15.4004 19.92188,9.96094 C19.92188,4.51172 15.4004,0 9.95118,0 C4.51172,0 0,4.51172 0,9.96094 C0,15.4004 4.52148,19.92188 9.96094,19.92188 Z M9.96094,18.261724 C5.35156,18.261724 1.66992,14.570324 1.66992,9.960944 C1.66992,5.351564 5.3418,1.660164 9.95116,1.660164 C14.56052,1.660164 18.2617,5.351564 18.2617,9.960944 C18.2617,14.570324 14.5703,18.261724 9.96092,18.261724 L9.96094,18.261724 Z M5.4297,9.960944 C5.4297,10.43946 5.761732,10.761726 6.259778,10.761726 L9.130878,10.761726 L9.130878,13.642586 C9.130878,14.130868 9.46291,14.472664 9.941424,14.472664 C10.4297,14.472664 10.771502,14.140632 10.771502,13.642586 L10.771502,10.761726 L13.652362,10.761726 C14.140644,10.761726 14.48244,10.43946 14.48244,9.960944 C14.48244,9.472662 14.140644,9.130866 13.652362,9.130866 L10.771502,9.130866 L10.771502,6.259766 C10.771502,5.76172 10.4297,5.419922 9.941424,5.419922 C9.462908,5.419922 9.130878,5.761718 9.130878,6.259766 L9.130878,9.130866 L6.259778,9.130866 C5.761732,9.130866 5.4297,9.472662 5.4297,9.960944 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(307.1798, 1913.2246)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M12.71486,25.43944 C19.67776,25.43944 25.43946,19.67772 25.43946,12.7246 C25.43946,5.7617 19.66798,0 12.70508,0 C5.75196,0 -1.42108547e-15,5.76172 -1.42108547e-15,12.7246 C-1.42108547e-15,19.67772 5.76172,25.43944 12.71484,25.43944 L12.71486,25.43944 Z M12.71486,23.623034 C6.6797,23.623034 1.82618,18.759754 1.82618,12.724594 C1.82618,6.679674 6.66994,1.826154 12.70508,1.826154 C18.75,1.826154 23.61328,6.679674 23.61328,12.724594 C23.61328,18.759754 18.75976,23.623034 12.71484,23.623034 L12.71486,23.623034 Z M6.94338,12.724594 C6.94338,13.242172 7.314474,13.6035 7.861348,13.6035 L11.806668,13.6035 L11.806668,17.55858 C11.806668,18.09569 12.177762,18.476548 12.69534,18.476548 C13.23245,18.476548 13.603544,18.105454 13.603544,17.55858 L13.603544,13.6035 L17.558624,13.6035 C18.095734,13.6035 18.476592,13.242172 18.476592,12.724594 C18.476592,12.177718 18.105498,11.806626 17.558624,11.806626 L13.603544,11.806626 L13.603544,7.861306 C13.603544,7.31443 13.23245,6.933572 12.69534,6.933572 C12.177762,6.933572 11.806668,7.314432 11.806668,7.861306 L11.806668,11.806626 L7.861348,11.806626 C7.314472,11.806626 6.94338,12.17772 6.94338,12.724594 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<text id="Design-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1953">Design Variations</tspan>
|
||||
</text>
|
||||
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
|
||||
</text>
|
||||
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
|
||||
</text>
|
||||
<text id="symbols-with-the-adjacent-text." fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
|
||||
</text>
|
||||
<line x1="776" y1="1919" x2="776" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<g id="Group" transform="translate(778.4902, 1918.7324)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0.8203116,14.423832 C1.3378896,14.423832 1.5917956,14.2285116 1.7773436,13.681636 L3.0371096,10.234376 L8.7988296,10.234376 L10.0585956,13.681636 C10.2441424,14.228512 10.4980496,14.423832 11.0058616,14.423832 C11.5234396,14.423832 11.8554716,14.111324 11.8554716,13.623042 C11.8554716,13.4570264 11.8261748,13.300776 11.7480498,13.095698 L7.1679698,0.898438 C6.9433598,0.302734 6.5429698,0 5.9179698,0 C5.3125018,0 4.9023458,0.292968 4.6875018,0.888672 L0.1074218,13.105472 C0.0292968,13.31055 -3.55271368e-16,13.4668 -3.55271368e-16,13.632816 C-3.55271368e-16,14.121098 0.3125,14.423832 0.820312,14.423832 L0.8203116,14.423832 Z M3.5156316,8.750004 L5.8886716,2.177744 L5.9374998,2.177744 L8.3105398,8.750004 L3.5156316,8.750004 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="792.836" y1="1919" x2="792.836" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5">
|
||||
</line>
|
||||
<text id="Margins" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="776" y="1953">Margins</tspan>
|
||||
</text>
|
||||
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol
|
||||
</tspan>
|
||||
</text>
|
||||
<text id="can-be-adjusted-by-modifying-the-x-location-of-the-margin-guidelines." fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1989">can be adjusted by modifying the x-location of the margin guidelines.
|
||||
</tspan>
|
||||
</text>
|
||||
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
|
||||
</text>
|
||||
<text id="scales-and-weights." fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="776" y="2025">scales and weights.</tspan>
|
||||
</text>
|
||||
<g id="Group" transform="translate(1291.2481, 1914.5174)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0.593687825,20.3477978 L2.29290583,22.0567818 C3.15228183,22.9259218 4.13860983,22.8673278 5.06634583,21.8419378 L15.7597058,10.0548378 L14.7929098,9.07827578 L4.17766983,20.7579558 C3.82610783,21.1583458 3.49407583,21.2560018 3.02532583,20.7872526 L1.85344983,19.6251426 C1.38469983,19.1661586 1.49212183,18.8243606 1.89251223,18.4630326 L13.3671122,7.66225258 L12.3905502,6.69545658 L0.798750225,17.5841366 C-0.187577775,18.5021046 -0.265703775,19.4786686 0.593672225,20.3478166 L0.593687825,20.3477978 Z M7.00970783,2.15443778 C6.58978583,2.56459378 6.56048983,3.14076578 6.79486383,3.53139178 C7.02923983,3.89271978 7.48822383,4.12709578 8.13275383,3.96107978 C9.59759783,3.61928378 11.1210338,3.56068978 12.5468138,4.49818978 L11.9608758,5.95326778 C11.6190798,6.78334578 11.7948602,7.36928378 12.3319698,7.91615778 L14.6268898,10.2306178 C15.1151718,10.7188998 15.5253278,10.7384298 16.0917338,10.6407738 L17.1561878,10.4454614 L17.8202498,11.1192894 L17.7811874,11.6759294 C17.742125,12.1739754 17.869078,12.5548354 18.3573594,13.0333514 L19.1190774,13.7755394 C19.5975934,14.2540554 20.2128274,14.2833514 20.6815774,13.8146018 L23.5917374,10.8946818 C24.0604874,10.4259318 24.0409554,9.83022778 23.5624406,9.35171378 L22.7909566,8.58999578 C22.3124406,8.11147978 21.9413466,7.95522978 21.4628326,7.99429178 L20.8866606,8.04311998 L20.2421286,7.40835398 L20.4862686,6.28530798 C20.6132218,5.71890198 20.4569718,5.27944798 19.8710346,4.69351198 L17.6737746,2.50601198 C14.3339346,-0.814308021 9.90033463,-0.736168021 7.00971463,2.15444998 L7.00970783,2.15443778 Z M8.50384783,2.52553178 C10.9354878,0.748187779 14.2265078,1.05092178 16.4530678,3.27748578 L18.8847078,5.68958578 C19.1190838,5.92396178 19.1581458,6.10950778 19.0897858,6.45130378 L18.7675198,7.93567978 L20.2714258,9.42005578 L21.2577538,9.36146198 C21.5116598,9.35169636 21.5897858,9.3712276 21.7850978,9.56653998 L22.3612698,10.142712 L19.9198698,12.584112 L19.3436978,12.00794 C19.1483854,11.8126276 19.1190878,11.734502 19.1288538,11.47083 L19.1972132,10.494268 L17.7030732,9.00989198 L16.1796352,9.26379798 C15.8573692,9.33215738 15.7108852,9.30286038 15.4667452,9.06848558 L13.4647852,7.06652558 C13.2108792,6.83214958 13.1815812,6.66613558 13.337832,6.29504158 L14.216738,4.20520158 C12.654238,2.75012358 10.622978,2.12512158 8.59173803,2.72082558 C8.43548803,2.75988798 8.37689403,2.63293498 8.50384743,2.52551318 L8.50384783,2.52553178 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<text id="Exporting" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1289" y="1953">Exporting</tspan>
|
||||
</text>
|
||||
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" fill-rule="nonzero"
|
||||
font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
|
||||
</text>
|
||||
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" fill-rule="nonzero"
|
||||
font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
|
||||
</text>
|
||||
<text id="template-version" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2952" y="1933">Template v.5.0</tspan>
|
||||
</text>
|
||||
<text id="Requires-Xcode-15-or-greater" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="2865" y="1951">Requires Xcode 15 or greater</tspan>
|
||||
</text>
|
||||
<text id="descriptive-name" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2835" y="1969">Generated from double.checkmark</tspan>
|
||||
</text>
|
||||
<text id="Typeset-at-100.0-points" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="2901" y="1987">Typeset at 100.0 points</tspan>
|
||||
</text>
|
||||
<text id="Small" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="726">Small</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1156">Medium</tspan>
|
||||
</text>
|
||||
<text id="Large" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1586">Large</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Guides" transform="translate(263, 600.785)">
|
||||
<g id="H-reference" transform="translate(76.9937, 24.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="95.215" x2="2773" y2="95.215" id="Baseline-S" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="24.756" x2="2773" y2="24.756" id="Capline-S" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<g id="H-reference" transform="translate(76.9937, 454.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="525.215" x2="2773" y2="525.215" id="Baseline-M" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="454.755" x2="2773" y2="454.755" id="Capline-M" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<g id="H-reference" transform="translate(76.9937, 884.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="955.215" x2="2773" y2="955.215" id="Baseline-L" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="884.755" x2="2773" y2="884.755" id="Capline-L" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="256.625" y1="1.13686838e-13" x2="256.625" y2="119.336" id="left-margin-Ultralight-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="348.798" y1="1.13686838e-13" x2="348.798" y2="119.336" id="right-margin-Ultralight-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="1143.53" y1="1.13686838e-13" x2="1143.53" y2="119.336" id="left-margin-Regular-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="1257.15" y1="1.13686838e-13" x2="1257.15" y2="119.336" id="right-margin-Regular-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="2622.62" y1="1.13686838e-13" x2="2622.62" y2="119.336" id="left-margin-Black-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="2760.18" y1="1.13686838e-13" x2="2760.18" y2="119.336" id="right-margin-Black-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
</g>
|
||||
<g id="Symbols" transform="translate(529.3906, 625.2969)" stroke="#000000" stroke-width="0.5">
|
||||
<g id="Black-S" transform="translate(2365.995, 0)">
|
||||
<path
|
||||
d="M67.46878,71.191381 C71.17968,71.191381 74.06058,69.873022 76.01368,66.99216 L111.07228,15.2343 C112.43948,13.2324 113.02538,11.1328 113.02538,9.2773 C113.02538,4.0039 108.82618,0 103.35738,0 C99.69528,0 97.30278,1.3183 95.05668,4.834 L67.32228,47.9492 L53.69918,32.6172 C51.79488,30.4687 49.54888,29.4433 46.52148,29.4433 C41.05278,29.4433 37,33.4472 37,38.7695 C37,41.2109 37.63478,43.1152 39.73438,45.459 L59.36328,67.67576 C61.51168,70.117162 64.14848,71.191381 67.46878,71.191381 Z"
|
||||
id="Path"></path>
|
||||
<path
|
||||
d="M9.52148,29.4433 C12.54888,29.4433 14.79488,30.4687 16.69918,32.6172 L30.32228,47.9492 L32.291,44.888 L44.484,58.915 L39.01368,66.99216 C37.1235832,69.780091 34.3645791,71.1046997 30.825305,71.1872572 L30.46878,71.191381 C27.14848,71.191381 24.51168,70.117162 22.36328,67.67576 L2.73438,45.459 C0.63478,43.1152 0,41.2109 0,38.7695 C0,33.4472 4.05278,29.4433 9.52148,29.4433 Z M66.35738,0 C71.82618,0 76.02538,4.0039 76.02538,9.2773 C76.02538,11.1328 75.43948,13.2324 74.07228,15.2343 L61.951,33.129 L49.252,18.52 L58.05668,4.834 C60.2386057,1.41874857 62.5586852,0.0771252245 66.0465687,0.00324899359 Z"
|
||||
id="Combined-Shape"></path>
|
||||
</g>
|
||||
<g id="Regular-S" transform="translate(886.905, 3.7109)">
|
||||
<path
|
||||
d="M55.87888,66.113294 C57.78318,66.113294 59.29688,65.28322 60.37108,63.62306 L95.96678,7.3242 C96.79688,6.0547 97.08988,5.0293 97.08988,4.0039 C97.08988,1.6113 95.52738,0 93.08598,0 C91.32818,0 90.35158,0.586 89.27738,2.2949 L55.68358,56.2012 L38.00778,32.3731 C36.88478,30.8594 35.81058,30.2246 34.19918,30.2246 C31.75778,30.2246 30,31.9336 30,34.375 C30,35.4004 30.43948,36.5234 31.26958,37.5977 L51.24028,63.5254 C52.60738,65.28322 53.97458,66.113294 55.87888,66.113294 Z"
|
||||
id="Path"></path>
|
||||
<path
|
||||
d="M4.19918,30.2246 C5.81058,30.2246 6.88478,30.8594 8.00778,32.3731 L25.68358,56.2012 L30.332,48.741 L35.554,55.426 L30.37108,63.62306 C29.3457073,65.2077582 27.919881,66.0361177 26.1361316,66.1081489 L25.87888,66.113294 C23.97458,66.113294 22.60738,65.28322 21.24028,63.5254 L1.26958,37.5977 C0.43948,36.5234 0,35.4004 0,34.375 C0,31.9336 1.75778,30.2246 4.19918,30.2246 Z M63.08598,0 C65.52738,0 67.08988,1.6113 67.08988,4.0039 C67.08988,5.0293 66.79688,6.0547 65.96678,7.3242 L48.893,34.328 L43.564,27.508 L59.27738,2.2949 C60.3068217,0.657204167 61.2466272,0.0507828125 62.8702602,0.00309092159 Z"
|
||||
id="Combined-Shape"></path>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="translate(0, 4.4375)">
|
||||
<path
|
||||
d="M36.79298,62.07178 C37.24418,62.07178 37.53178,61.87744 37.78868,61.53417 L76.33598,2.0112 C76.57578,1.6045 76.64168,1.3965 76.64168,1.1885 C76.64168,0.5215 76.12358,0 75.45318,0 C75.01228,0 74.71678,0.1772 74.50538,0.5693 L36.73398,58.97114 L18.24078,37.7768 C17.98048,37.3984 17.67828,37.2177 17.20218,37.2177 C16.48638,37.2177 16,37.7006 16,38.371 C16,38.6699 16.12159,38.9755 16.40678,39.2778 L35.65088,61.52733 C36.01908,61.96826 36.29638,62.07178 36.79298,62.07178 Z"
|
||||
id="Path"></path>
|
||||
<path
|
||||
d="M1.20218,37.2177 C1.67828,37.2177 1.98048,37.3984 2.24078,37.7768 L20.73398,58.97114 L23.078,55.345 L24.636,57.137 L21.78868,61.53417 C21.5603244,61.8392989 21.3077121,62.0267547 20.937503,62.0646445 L20.79298,62.07178 C20.29638,62.07178 20.01908,61.96826 19.65088,61.52733 L0.40678,39.2778 C0.12159,38.9755 0,38.6699 0,38.371 C0,37.7006 0.48638,37.2177 1.20218,37.2177 Z M59.45318,0 C60.12358,0 60.64168,0.5215 60.64168,1.1885 C60.64168,1.3965 60.57578,1.6045 60.33598,2.0112 L32.216,45.432 L30.652,43.634 L58.50538,0.5693 C58.6932911,0.220766667 58.9476516,0.0420308642 59.3115144,0.00661467764 Z"
|
||||
id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
},
|
||||
"symbols": [
|
||||
{
|
||||
"filename": "checkmark.wide.svg",
|
||||
"idiom": "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>checkmark.wide</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="double.checkmark">
|
||||
<g id="Notes">
|
||||
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
|
||||
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<text id="Weight/Scale-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="322">Weight/Scale Variations</tspan>
|
||||
</text>
|
||||
<text id="Ultralight" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="533.711" y="322">Ultralight</tspan>
|
||||
</text>
|
||||
<text id="Thin" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="843.422" y="322">Thin</tspan>
|
||||
</text>
|
||||
<text id="Light" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1138.63" y="322">Light</tspan>
|
||||
</text>
|
||||
<text id="Regular" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1426.84" y="322">Regular</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1723.06" y="322">Medium</tspan>
|
||||
</text>
|
||||
<text id="Semibold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2015.77" y="322">Semibold</tspan>
|
||||
</text>
|
||||
<text id="Bold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2326.48" y="322">Bold</tspan>
|
||||
</text>
|
||||
<text id="Heavy" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2618.19" y="322">Heavy</tspan>
|
||||
</text>
|
||||
<text id="Black" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2917.4" y="322">Black</tspan>
|
||||
</text>
|
||||
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
|
||||
<g id="Group" transform="translate(264.3672, 1918.0684)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M7.88088,15.76172 C12.18752,15.76172 15.7715,12.1875 15.7715,7.88086 C15.7715,3.57422 12.17774,0 7.8711,0 C3.57422,0 0,3.57422 0,7.88086 C0,12.1875 3.58398,15.76172 7.88086,15.76172 L7.88088,15.76172 Z M7.88088,14.277344 C4.33596,14.277344 1.50392,11.435544 1.50392,7.880864 C1.50392,4.326184 4.32618,1.484384 7.8711,1.484384 C11.42578,1.484384 14.27734,4.326184 14.27734,7.880864 C14.27734,11.435544 11.43554,14.277344 7.88086,14.277344 L7.88088,14.277344 Z M4.28712,7.880864 C4.28712,8.310552 4.589854,8.60352 5.039074,8.60352 L7.138674,8.60352 L7.138674,10.72266 C7.138674,11.162114 7.431642,11.464848 7.86133,11.464848 C8.310548,11.464848 8.603518,11.162114 8.603518,10.72266 L8.603518,8.60352 L10.722658,8.60352 C11.162112,8.60352 11.464846,8.310552 11.464846,7.880864 C11.464846,7.44141 11.162112,7.138676 10.722658,7.138676 L8.603518,7.138676 L8.603518,5.029296 C8.603518,4.580078 8.31055,4.277342 7.86133,4.277342 C7.431642,4.277342 7.138674,4.580076 7.138674,5.029296 L7.138674,7.138676 L5.039074,7.138676 C4.589856,7.138676 4.28712,7.44141 4.28712,7.880864 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(283.254, 1915.9883)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M9.96094,19.92188 C15.41016,19.92188 19.92188,15.4004 19.92188,9.96094 C19.92188,4.51172 15.4004,0 9.95118,0 C4.51172,0 0,4.51172 0,9.96094 C0,15.4004 4.52148,19.92188 9.96094,19.92188 Z M9.96094,18.261724 C5.35156,18.261724 1.66992,14.570324 1.66992,9.960944 C1.66992,5.351564 5.3418,1.660164 9.95116,1.660164 C14.56052,1.660164 18.2617,5.351564 18.2617,9.960944 C18.2617,14.570324 14.5703,18.261724 9.96092,18.261724 L9.96094,18.261724 Z M5.4297,9.960944 C5.4297,10.43946 5.761732,10.761726 6.259778,10.761726 L9.130878,10.761726 L9.130878,13.642586 C9.130878,14.130868 9.46291,14.472664 9.941424,14.472664 C10.4297,14.472664 10.771502,14.140632 10.771502,13.642586 L10.771502,10.761726 L13.652362,10.761726 C14.140644,10.761726 14.48244,10.43946 14.48244,9.960944 C14.48244,9.472662 14.140644,9.130866 13.652362,9.130866 L10.771502,9.130866 L10.771502,6.259766 C10.771502,5.76172 10.4297,5.419922 9.941424,5.419922 C9.462908,5.419922 9.130878,5.761718 9.130878,6.259766 L9.130878,9.130866 L6.259778,9.130866 C5.761732,9.130866 5.4297,9.472662 5.4297,9.960944 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<g id="Group" transform="translate(307.1798, 1913.2246)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M12.71486,25.43944 C19.67776,25.43944 25.43946,19.67772 25.43946,12.7246 C25.43946,5.7617 19.66798,0 12.70508,0 C5.75196,0 -1.42108547e-15,5.76172 -1.42108547e-15,12.7246 C-1.42108547e-15,19.67772 5.76172,25.43944 12.71484,25.43944 L12.71486,25.43944 Z M12.71486,23.623034 C6.6797,23.623034 1.82618,18.759754 1.82618,12.724594 C1.82618,6.679674 6.66994,1.826154 12.70508,1.826154 C18.75,1.826154 23.61328,6.679674 23.61328,12.724594 C23.61328,18.759754 18.75976,23.623034 12.71484,23.623034 L12.71486,23.623034 Z M6.94338,12.724594 C6.94338,13.242172 7.314474,13.6035 7.861348,13.6035 L11.806668,13.6035 L11.806668,17.55858 C11.806668,18.09569 12.177762,18.476548 12.69534,18.476548 C13.23245,18.476548 13.603544,18.105454 13.603544,17.55858 L13.603544,13.6035 L17.558624,13.6035 C18.095734,13.6035 18.476592,13.242172 18.476592,12.724594 C18.476592,12.177718 18.105498,11.806626 17.558624,11.806626 L13.603544,11.806626 L13.603544,7.861306 C13.603544,7.31443 13.23245,6.933572 12.69534,6.933572 C12.177762,6.933572 11.806668,7.314432 11.806668,7.861306 L11.806668,11.806626 L7.861348,11.806626 C7.314472,11.806626 6.94338,12.17772 6.94338,12.724594 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<text id="Design-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1953">Design Variations</tspan>
|
||||
</text>
|
||||
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
|
||||
</text>
|
||||
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
|
||||
</text>
|
||||
<text id="symbols-with-the-adjacent-text." fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
|
||||
</text>
|
||||
<line x1="776" y1="1919" x2="776" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<g id="Group" transform="translate(778.4902, 1918.7324)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0.8203116,14.423832 C1.3378896,14.423832 1.5917956,14.2285116 1.7773436,13.681636 L3.0371096,10.234376 L8.7988296,10.234376 L10.0585956,13.681636 C10.2441424,14.228512 10.4980496,14.423832 11.0058616,14.423832 C11.5234396,14.423832 11.8554716,14.111324 11.8554716,13.623042 C11.8554716,13.4570264 11.8261748,13.300776 11.7480498,13.095698 L7.1679698,0.898438 C6.9433598,0.302734 6.5429698,0 5.9179698,0 C5.3125018,0 4.9023458,0.292968 4.6875018,0.888672 L0.1074218,13.105472 C0.0292968,13.31055 -3.55271368e-16,13.4668 -3.55271368e-16,13.632816 C-3.55271368e-16,14.121098 0.3125,14.423832 0.820312,14.423832 L0.8203116,14.423832 Z M3.5156316,8.750004 L5.8886716,2.177744 L5.9374998,2.177744 L8.3105398,8.750004 L3.5156316,8.750004 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="792.836" y1="1919" x2="792.836" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5">
|
||||
</line>
|
||||
<text id="Margins" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="776" y="1953">Margins</tspan>
|
||||
</text>
|
||||
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol
|
||||
</tspan>
|
||||
</text>
|
||||
<text id="can-be-adjusted-by-modifying-the-x-location-of-the-margin-guidelines." fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="1989">can be adjusted by modifying the x-location of the margin guidelines.
|
||||
</tspan>
|
||||
</text>
|
||||
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000"
|
||||
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
|
||||
</text>
|
||||
<text id="scales-and-weights." fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="776" y="2025">scales and weights.</tspan>
|
||||
</text>
|
||||
<g id="Group" transform="translate(1291.2481, 1914.5174)" fill="#000000" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0.593687825,20.3477978 L2.29290583,22.0567818 C3.15228183,22.9259218 4.13860983,22.8673278 5.06634583,21.8419378 L15.7597058,10.0548378 L14.7929098,9.07827578 L4.17766983,20.7579558 C3.82610783,21.1583458 3.49407583,21.2560018 3.02532583,20.7872526 L1.85344983,19.6251426 C1.38469983,19.1661586 1.49212183,18.8243606 1.89251223,18.4630326 L13.3671122,7.66225258 L12.3905502,6.69545658 L0.798750225,17.5841366 C-0.187577775,18.5021046 -0.265703775,19.4786686 0.593672225,20.3478166 L0.593687825,20.3477978 Z M7.00970783,2.15443778 C6.58978583,2.56459378 6.56048983,3.14076578 6.79486383,3.53139178 C7.02923983,3.89271978 7.48822383,4.12709578 8.13275383,3.96107978 C9.59759783,3.61928378 11.1210338,3.56068978 12.5468138,4.49818978 L11.9608758,5.95326778 C11.6190798,6.78334578 11.7948602,7.36928378 12.3319698,7.91615778 L14.6268898,10.2306178 C15.1151718,10.7188998 15.5253278,10.7384298 16.0917338,10.6407738 L17.1561878,10.4454614 L17.8202498,11.1192894 L17.7811874,11.6759294 C17.742125,12.1739754 17.869078,12.5548354 18.3573594,13.0333514 L19.1190774,13.7755394 C19.5975934,14.2540554 20.2128274,14.2833514 20.6815774,13.8146018 L23.5917374,10.8946818 C24.0604874,10.4259318 24.0409554,9.83022778 23.5624406,9.35171378 L22.7909566,8.58999578 C22.3124406,8.11147978 21.9413466,7.95522978 21.4628326,7.99429178 L20.8866606,8.04311998 L20.2421286,7.40835398 L20.4862686,6.28530798 C20.6132218,5.71890198 20.4569718,5.27944798 19.8710346,4.69351198 L17.6737746,2.50601198 C14.3339346,-0.814308021 9.90033463,-0.736168021 7.00971463,2.15444998 L7.00970783,2.15443778 Z M8.50384783,2.52553178 C10.9354878,0.748187779 14.2265078,1.05092178 16.4530678,3.27748578 L18.8847078,5.68958578 C19.1190838,5.92396178 19.1581458,6.10950778 19.0897858,6.45130378 L18.7675198,7.93567978 L20.2714258,9.42005578 L21.2577538,9.36146198 C21.5116598,9.35169636 21.5897858,9.3712276 21.7850978,9.56653998 L22.3612698,10.142712 L19.9198698,12.584112 L19.3436978,12.00794 C19.1483854,11.8126276 19.1190878,11.734502 19.1288538,11.47083 L19.1972132,10.494268 L17.7030732,9.00989198 L16.1796352,9.26379798 C15.8573692,9.33215738 15.7108852,9.30286038 15.4667452,9.06848558 L13.4647852,7.06652558 C13.2108792,6.83214958 13.1815812,6.66613558 13.337832,6.29504158 L14.216738,4.20520158 C12.654238,2.75012358 10.622978,2.12512158 8.59173803,2.72082558 C8.43548803,2.75988798 8.37689403,2.63293498 8.50384743,2.52551318 L8.50384783,2.52553178 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<text id="Exporting" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="1289" y="1953">Exporting</tspan>
|
||||
</text>
|
||||
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" fill-rule="nonzero"
|
||||
font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
|
||||
</text>
|
||||
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" fill-rule="nonzero"
|
||||
font-family="Helvetica" font-size="13" font-weight="normal">
|
||||
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
|
||||
</text>
|
||||
<text id="template-version" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2952" y="1933">Template v.5.0</tspan>
|
||||
</text>
|
||||
<text id="Requires-Xcode-15-or-greater" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="2865" y="1951">Requires Xcode 15 or greater</tspan>
|
||||
</text>
|
||||
<text id="descriptive-name" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="2835" y="1969">Generated from double.checkmark</tspan>
|
||||
</text>
|
||||
<text id="Typeset-at-100.0-points" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
|
||||
font-size="13" font-weight="normal">
|
||||
<tspan x="2901" y="1987">Typeset at 100.0 points</tspan>
|
||||
</text>
|
||||
<text id="Small" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="726">Small</tspan>
|
||||
</text>
|
||||
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1156">Medium</tspan>
|
||||
</text>
|
||||
<text id="Large" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
|
||||
font-weight="normal">
|
||||
<tspan x="263" y="1586">Large</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Guides" transform="translate(263, 600.785)">
|
||||
<g id="H-reference" transform="translate(76.9937, 24.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="95.215" x2="2773" y2="95.215" id="Baseline-S" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="24.756" x2="2773" y2="24.756" id="Capline-S" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<g id="H-reference" transform="translate(76.9937, 454.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="525.215" x2="2773" y2="525.215" id="Baseline-M" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="454.755" x2="2773" y2="454.755" id="Capline-M" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<g id="H-reference" transform="translate(76.9937, 884.756)" fill="#27AAE1" fill-rule="nonzero">
|
||||
<path
|
||||
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
|
||||
id="Shape"></path>
|
||||
</g>
|
||||
<line x1="0" y1="955.215" x2="2773" y2="955.215" id="Baseline-L" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="0" y1="884.755" x2="2773" y2="884.755" id="Capline-L" stroke="#27AAE1" stroke-width="0.5">
|
||||
</line>
|
||||
<line x1="256.625" y1="1.13686838e-13" x2="256.625" y2="119.336" id="left-margin-Ultralight-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="348.798" y1="1.13686838e-13" x2="348.798" y2="119.336" id="right-margin-Ultralight-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="1143.53" y1="1.13686838e-13" x2="1143.53" y2="119.336" id="left-margin-Regular-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="1257.15" y1="1.13686838e-13" x2="1257.15" y2="119.336" id="right-margin-Regular-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="2622.62" y1="1.13686838e-13" x2="2622.62" y2="119.336" id="left-margin-Black-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
<line x1="2760.18" y1="1.13686838e-13" x2="2760.18" y2="119.336" id="right-margin-Black-S"
|
||||
stroke="#00AEEF" stroke-width="0.5"></line>
|
||||
</g>
|
||||
<g id="Symbols" transform="translate(529.3906, 625.2969)" stroke="#000000" stroke-width="0.5">
|
||||
<g id="Black-S" transform="translate(2365.995, 0)">
|
||||
<path
|
||||
d="M30.46878,71.191381 C34.17968,71.191381 37.06058,69.873022 39.01368,66.99216 L74.07228,15.2343 C75.43948,13.2324 76.02538,11.1328 76.02538,9.2773 C76.02538,4.0039 71.82618,0 66.35738,0 C62.69528,0 60.30278,1.3183 58.05668,4.834 L30.32228,47.9492 L16.69918,32.6172 C14.79488,30.4687 12.54888,29.4433 9.52148,29.4433 C4.05278,29.4433 0,33.4472 0,38.7695 C0,41.2109 0.63478,43.1152 2.73438,45.459 L22.36328,67.67576 C24.51168,70.117162 27.14848,71.191381 30.46878,71.191381 Z"
|
||||
id="Path"></path>
|
||||
</g>
|
||||
<g id="Regular-S" transform="translate(886.905, 3.7109)">
|
||||
<path
|
||||
d="M25.87888,66.113294 C27.78318,66.113294 29.29688,65.28322 30.37108,63.62306 L65.96678,7.3242 C66.79688,6.0547 67.08988,5.0293 67.08988,4.0039 C67.08988,1.6113 65.52738,0 63.08598,0 C61.32818,0 60.35158,0.586 59.27738,2.2949 L25.68358,56.2012 L8.00778,32.3731 C6.88478,30.8594 5.81058,30.2246 4.19918,30.2246 C1.75778,30.2246 0,31.9336 0,34.375 C0,35.4004 0.43948,36.5234 1.26958,37.5977 L21.24028,63.5254 C22.60738,65.28322 23.97458,66.113294 25.87888,66.113294 Z"
|
||||
id="Path"></path>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="translate(0, 4.4375)">
|
||||
<path
|
||||
d="M20.79298,62.07178 C21.24418,62.07178 21.53178,61.87744 21.78868,61.53417 L60.33598,2.0112 C60.57578,1.6045 60.64168,1.3965 60.64168,1.1885 C60.64168,0.5215 60.12358,0 59.45318,0 C59.01228,0 58.71678,0.1772 58.50538,0.5693 L20.73398,58.97114 L2.24078,37.7768 C1.98048,37.3984 1.67828,37.2177 1.20218,37.2177 C0.48638,37.2177 0,37.7006 0,38.371 C0,38.6699 0.12159,38.9755 0.40678,39.2778 L19.65088,61.52733 C20.01908,61.96826 20.29638,62.07178 20.79298,62.07178 Z"
|
||||
id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 22 KiB |
@@ -888,6 +888,35 @@ final class ChatModel: ObservableObject {
|
||||
_ = upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
|
||||
var i = 0
|
||||
var totalBelow = 0
|
||||
var unreadBelow = 0
|
||||
while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
|
||||
totalBelow += 1
|
||||
if im.reversedChatItems[i].isRcvNew {
|
||||
unreadBelow += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return UnreadChatItemCounts(
|
||||
// TODO these thresholds account for the fact that items are still "visible" while
|
||||
// covered by compose area, they should be replaced with the actual height in pixels below the screen.
|
||||
isNearBottom: totalBelow < 15,
|
||||
isReallyNearBottom: totalBelow < 2,
|
||||
unreadBelow: unreadBelow
|
||||
)
|
||||
}
|
||||
|
||||
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
||||
let maxIx = im.reversedChatItems.count - 1
|
||||
var i = 0
|
||||
let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) }
|
||||
while i < maxIx && !inView(i) { i += 1 }
|
||||
while i < maxIx && inView(i) { i += 1 }
|
||||
return im.reversedChatItems[min(i - 1, maxIx)]
|
||||
}
|
||||
}
|
||||
|
||||
struct ShowingInvitation {
|
||||
@@ -900,6 +929,12 @@ struct NTFContactRequest {
|
||||
var chatId: String
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var isNearBottom: Bool
|
||||
var isReallyNearBottom: Bool
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
@Published var chatInfo: ChatInfo
|
||||
@Published var chatItems: [ChatItem]
|
||||
|
||||
@@ -585,13 +585,13 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
@@ -2199,11 +2199,9 @@ func refreshCallInvitations() async throws {
|
||||
}
|
||||
}
|
||||
|
||||
func justRefreshCallInvitations() async throws {
|
||||
func justRefreshCallInvitations() throws {
|
||||
let callInvitations = try apiGetCallInvitationsSync()
|
||||
await MainActor.run {
|
||||
ChatModel.shared.callInvitations = callsByChat(callInvitations)
|
||||
}
|
||||
ChatModel.shared.callInvitations = callsByChat(callInvitations)
|
||||
}
|
||||
|
||||
private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] {
|
||||
@@ -2213,13 +2211,12 @@ private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: Rcv
|
||||
}
|
||||
|
||||
func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
if !callInvitation.user.showNotifications { return }
|
||||
let m = ChatModel.shared
|
||||
logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))")
|
||||
if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return }
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
m.callInvitations[callInvitation.contact.id]?.callUUID = nil
|
||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||
}
|
||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||
} else {
|
||||
|
||||
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
|
||||
public func importFromString() -> ThemeWallpaper {
|
||||
if preset == nil, let image {
|
||||
// Need to save image from string and to save its path
|
||||
if let parsed = imageFromBase64(image),
|
||||
if let parsed = UIImage(base64Encoded: image),
|
||||
let filename = saveWallpaperFile(image: parsed) {
|
||||
var copy = self
|
||||
copy.image = nil
|
||||
|
||||
@@ -185,7 +185,7 @@ struct ActiveCallView: View {
|
||||
case .ended:
|
||||
closeCallView(client)
|
||||
call.callState = .ended
|
||||
if let uuid = call.callUUID {
|
||||
if let uuid = call.callkitUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
}
|
||||
case .ok:
|
||||
@@ -382,7 +382,7 @@ struct ActiveCallOverlay: View {
|
||||
private func endCallButton() -> some View {
|
||||
let cc = CallController.shared
|
||||
return callButton("phone.down.fill", width: 60, height: 60) {
|
||||
if let uuid = call.callUUID {
|
||||
if let uuid = call.callkitUUID {
|
||||
cc.endCall(callUUID: uuid)
|
||||
} else {
|
||||
cc.endCall(call: call) {}
|
||||
@@ -462,9 +462,9 @@ struct ActiveCallOverlay: View {
|
||||
struct ActiveCallOverlay_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
logger.debug("CallController.provider CXStartCallAction")
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
} else {
|
||||
@@ -61,30 +61,12 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
logger.debug("CallController.provider CXAnswerCallAction")
|
||||
Task {
|
||||
let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500)
|
||||
logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))")
|
||||
if !chatIsReady {
|
||||
action.fail()
|
||||
return
|
||||
}
|
||||
if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) {
|
||||
try? await justRefreshCallInvitations()
|
||||
logger.debug("CallController: updated call invitations chat")
|
||||
}
|
||||
await MainActor.run {
|
||||
logger.debug("CallController.provider will answer on call")
|
||||
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
logger.debug("CallController.provider answered on call")
|
||||
// WebRTC call should be in connected state to fulfill.
|
||||
// Otherwise no audio and mic working on lockscreen
|
||||
fulfillOnConnect = action
|
||||
} else {
|
||||
logger.debug("CallController.provider will fail the call")
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// WebRTC call should be in connected state to fulfill.
|
||||
// Otherwise no audio and mic working on lockscreen
|
||||
fulfillOnConnect = action
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +75,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
// Should be nil here if connection was in connected state
|
||||
fulfillOnConnect?.fail()
|
||||
fulfillOnConnect = nil
|
||||
callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in
|
||||
callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
if ok {
|
||||
action.fulfill()
|
||||
} else {
|
||||
@@ -104,7 +86,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
@@ -174,19 +156,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
}
|
||||
|
||||
private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool {
|
||||
logger.debug("CallController waiting until chat started")
|
||||
var t: UInt64 = 0
|
||||
repeat {
|
||||
if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value {
|
||||
return true
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
|
||||
t += stepMs
|
||||
} while t < timeoutMs
|
||||
return false
|
||||
}
|
||||
|
||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
|
||||
@@ -202,19 +171,32 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat")
|
||||
do {
|
||||
try initializeChat(start: true, refreshInvitations: false)
|
||||
} catch let error {
|
||||
logger.error("CallController: initializing chat error: \(error)")
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.debug("CallController: initialized chat")
|
||||
startChatForCall()
|
||||
logger.debug("CallController: started chat")
|
||||
self.shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
try? justRefreshCallInvitations()
|
||||
logger.debug("CallController: updated call invitations chat")
|
||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||
// Extract the call information from the push notification payload
|
||||
let m = ChatModel.shared
|
||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
let displayName = payload.dictionaryPayload["displayName"] as? String,
|
||||
let callUUID = payload.dictionaryPayload["callUUID"] as? String,
|
||||
let uuid = UUID(uuidString: callUUID),
|
||||
let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval,
|
||||
let mediaStr = payload.dictionaryPayload["media"] as? String,
|
||||
let media = CallMediaType(rawValue: mediaStr) {
|
||||
let update = self.cxCallUpdate(contactId, displayName, media)
|
||||
let callTs = Date(timeIntervalSince1970: callTsInterval)
|
||||
if callTs.timeIntervalSinceNow >= -180 {
|
||||
let invitation = m.callInvitations[contactId] {
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
@@ -223,31 +205,11 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
logger.debug("CallController will expire call 1")
|
||||
self.reportExpiredCall(update: update, completion)
|
||||
}
|
||||
} else {
|
||||
logger.debug("CallController will expire call 2")
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
}
|
||||
|
||||
//DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat")
|
||||
do {
|
||||
try initializeChat(start: true, refreshInvitations: false)
|
||||
} catch let error {
|
||||
logger.error("CallController: initializing chat error: \(error)")
|
||||
if let call = ChatModel.shared.activeCall {
|
||||
self.endCall(call: call, completed: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.debug("CallController: initialized chat")
|
||||
startChatForCall()
|
||||
logger.debug("CallController: started chat")
|
||||
self.shouldSuspendChat = true
|
||||
}
|
||||
|
||||
// This function fulfils the requirement to always report a call when PushKit notification is received,
|
||||
@@ -277,8 +239,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
|
||||
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
@@ -299,14 +261,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
return update
|
||||
}
|
||||
|
||||
private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: contactId)
|
||||
update.hasVideo = media == .video
|
||||
update.localizedCallerName = displayName
|
||||
return update
|
||||
}
|
||||
|
||||
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting incoming call connected")
|
||||
if CallController.useCallKit() {
|
||||
@@ -318,14 +272,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting outgoing call connected")
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
}
|
||||
}
|
||||
|
||||
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
} else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
activeCallInvitation = nil
|
||||
@@ -334,17 +288,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func reportCallRemoteEnded(call: Call) {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(_ contact: Contact, _ media: CallMediaType) {
|
||||
logger.debug("CallController.startCall")
|
||||
let callUUID = callManager.newOutgoingCall(contact, media)
|
||||
guard let uuid = UUID(uuidString: callUUID) else {
|
||||
return
|
||||
}
|
||||
let uuid = callManager.newOutgoingCall(contact, media)
|
||||
if CallController.useCallKit() {
|
||||
let handle = CXHandle(type: .generic, value: contact.id)
|
||||
let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
@@ -356,8 +307,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
update.localizedCallerName = contact.displayName
|
||||
self.provider.reportCall(with: uuid, updated: update)
|
||||
}
|
||||
} else if callManager.startOutgoingCall(callUUID: callUUID) {
|
||||
if callManager.startOutgoingCall(callUUID: callUUID) {
|
||||
} else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
@@ -367,8 +318,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func answerCall(invitation: RcvCallInvitation) {
|
||||
logger.debug("CallController: answering a call")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
requestTransaction(with: CXAnswerCallAction(call: uuid))
|
||||
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
|
||||
requestTransaction(with: CXAnswerCallAction(call: callUUID))
|
||||
} else {
|
||||
callManager.answerIncomingCall(invitation: invitation)
|
||||
}
|
||||
@@ -377,13 +328,10 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(callUUID: String) {
|
||||
let uuid = UUID(uuidString: callUUID)
|
||||
logger.debug("CallController: ending the call with UUID \(callUUID)")
|
||||
func endCall(callUUID: UUID) {
|
||||
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
|
||||
if CallController.useCallKit() {
|
||||
if let uuid {
|
||||
requestTransaction(with: CXEndCallAction(call: uuid))
|
||||
}
|
||||
requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
} else {
|
||||
callManager.endCall(callUUID: callUUID) { ok in
|
||||
if ok {
|
||||
|
||||
@@ -10,17 +10,17 @@ import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
class CallManager {
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
|
||||
let uuid = UUID().uuidString.lowercased()
|
||||
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
|
||||
let uuid = UUID()
|
||||
let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
call.speakerEnabled = media == .video
|
||||
ChatModel.shared.activeCall = call
|
||||
return uuid
|
||||
}
|
||||
|
||||
func startOutgoingCall(callUUID: String) -> Bool {
|
||||
func startOutgoingCall(callUUID: UUID) -> Bool {
|
||||
let m = ChatModel.shared
|
||||
if let call = m.activeCall, call.callUUID == callUUID {
|
||||
if let call = m.activeCall, call.callkitUUID == callUUID {
|
||||
m.showCallView = true
|
||||
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
|
||||
return true
|
||||
@@ -28,7 +28,7 @@ class CallManager {
|
||||
return false
|
||||
}
|
||||
|
||||
func answerIncomingCall(callUUID: String) -> Bool {
|
||||
func answerIncomingCall(callUUID: UUID) -> Bool {
|
||||
if let invitation = getCallInvitation(callUUID) {
|
||||
answerIncomingCall(invitation: invitation)
|
||||
return true
|
||||
@@ -42,7 +42,7 @@ class CallManager {
|
||||
let call = Call(
|
||||
direction: .incoming,
|
||||
contact: invitation.contact,
|
||||
callUUID: invitation.callUUID,
|
||||
callkitUUID: invitation.callkitUUID,
|
||||
callState: .invitationAccepted,
|
||||
localMedia: invitation.callType.media,
|
||||
sharedKey: invitation.sharedKey
|
||||
@@ -68,8 +68,8 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool {
|
||||
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
|
||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||
let m = ChatModel.shared
|
||||
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
|
||||
return true
|
||||
@@ -77,8 +77,8 @@ class CallManager {
|
||||
return false
|
||||
}
|
||||
|
||||
func endCall(callUUID: String, completed: @escaping (Bool) -> Void) {
|
||||
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
|
||||
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
|
||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||
endCall(call: call) { completed(true) }
|
||||
} else if let invitation = getCallInvitation(callUUID) {
|
||||
endCall(invitation: invitation) { completed(true) }
|
||||
@@ -126,8 +126,8 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? {
|
||||
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) {
|
||||
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
|
||||
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
|
||||
return invitation
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -18,7 +18,7 @@ class Call: ObservableObject, Equatable {
|
||||
|
||||
var direction: CallDirection
|
||||
var contact: Contact
|
||||
var callUUID: String?
|
||||
var callkitUUID: UUID?
|
||||
var localMedia: CallMediaType
|
||||
@Published var callState: CallState
|
||||
@Published var localCapabilities: CallCapabilities?
|
||||
@@ -33,14 +33,14 @@ class Call: ObservableObject, Equatable {
|
||||
init(
|
||||
direction: CallDirection,
|
||||
contact: Contact,
|
||||
callUUID: String?,
|
||||
callkitUUID: UUID?,
|
||||
callState: CallState,
|
||||
localMedia: CallMediaType,
|
||||
sharedKey: String? = nil
|
||||
) {
|
||||
self.direction = direction
|
||||
self.contact = contact
|
||||
self.callUUID = callUUID
|
||||
self.callkitUUID = callkitUUID
|
||||
self.callState = callState
|
||||
self.localMedia = localMedia
|
||||
self.sharedKey = sharedKey
|
||||
|
||||
@@ -942,7 +942,7 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
|
||||
)
|
||||
}
|
||||
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String {
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
|
||||
let (rcvMsgInfo, qInfo) = info
|
||||
var msgInfo: String
|
||||
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
|
||||
|
||||
@@ -12,7 +12,7 @@ import SimpleXChat
|
||||
struct CIGroupInvitationView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.showTimestamp) var showTimestamp
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var groupInvitation: CIGroupInvitation
|
||||
@@ -46,7 +46,7 @@ struct CIGroupInvitationView: View {
|
||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -54,7 +54,7 @@ struct CIGroupInvitationView: View {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -70,7 +70,8 @@ struct CIGroupInvitationView: View {
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.textSelection(.disabled)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
.onChange(of: inProgress) { inProgress in
|
||||
|
||||
@@ -165,9 +165,9 @@ struct CIImageView: View {
|
||||
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.invertedForegroundStyle()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.white)
|
||||
.padding(padding)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ struct CILinkView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 6) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
||||
@@ -12,13 +12,12 @@ import SimpleXChat
|
||||
struct CIMetaView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.showTimestamp) var showTimestamp
|
||||
var chatItem: ChatItem
|
||||
var metaColor: Color
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
var invertedMaterial = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
@@ -26,138 +25,96 @@ struct CIMetaView: View {
|
||||
if chatItem.isDeletedContent {
|
||||
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
|
||||
} else {
|
||||
ZStack {
|
||||
ciMetaText(
|
||||
chatItem.meta,
|
||||
chatTTL: chat.chatInfo.timedMessagesTTL,
|
||||
encrypted: chatItem.encryptedFile,
|
||||
color: metaColor,
|
||||
paleColor: paleMetaColor,
|
||||
colorMode: invertedMaterial
|
||||
? .invertedMaterial
|
||||
: .normal,
|
||||
showStatus: showStatus,
|
||||
showEdited: showEdited,
|
||||
showViaProxy: showSentViaProxy,
|
||||
showTimesamp: showTimestamp
|
||||
).invertedForegroundStyle(enabled: invertedMaterial)
|
||||
if invertedMaterial {
|
||||
ciMetaText(
|
||||
chatItem.meta,
|
||||
chatTTL: chat.chatInfo.timedMessagesTTL,
|
||||
encrypted: chatItem.encryptedFile,
|
||||
colorMode: .normal,
|
||||
onlyOverrides: true,
|
||||
showStatus: showStatus,
|
||||
showEdited: showEdited,
|
||||
showViaProxy: showSentViaProxy,
|
||||
showTimesamp: showTimestamp
|
||||
)
|
||||
let meta = chatItem.meta
|
||||
let ttl = chat.chatInfo.timedMessagesTTL
|
||||
let encrypted = chatItem.encryptedFile
|
||||
switch meta.itemStatus {
|
||||
case let .sndSent(sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
case let .sndRcvd(_, sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
case .partial:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
default:
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MetaColorMode {
|
||||
// Renders provided colours
|
||||
case normal
|
||||
// Fully transparent meta - used for reserving space
|
||||
case transparent
|
||||
// Renders white on dark backgrounds and black on light ones
|
||||
case invertedMaterial
|
||||
|
||||
func resolve(_ c: Color?) -> Color? {
|
||||
switch self {
|
||||
case .normal: c
|
||||
case .transparent: .clear
|
||||
case .invertedMaterial: nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusSpacer: Text {
|
||||
switch self {
|
||||
case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear)
|
||||
case .invertedMaterial: Text(" ").kerning(13)
|
||||
}
|
||||
}
|
||||
enum SentCheckmark {
|
||||
case sent
|
||||
case rcvd1
|
||||
case rcvd2
|
||||
}
|
||||
|
||||
func ciMetaText(
|
||||
_ meta: CIMeta,
|
||||
chatTTL: Int?,
|
||||
encrypted: Bool?,
|
||||
color: Color = .clear, // we use this function to reserve space without rendering meta
|
||||
paleColor: Color? = nil,
|
||||
color: Color = .clear,
|
||||
primaryColor: Color = .accentColor,
|
||||
colorMode: MetaColorMode = .normal,
|
||||
onlyOverrides: Bool = false, // only render colors that differ from base
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
showEdited: Bool = true,
|
||||
showViaProxy: Bool,
|
||||
showTimesamp: Bool
|
||||
) -> Text {
|
||||
var r = Text("")
|
||||
var space: Text? = nil
|
||||
let appendSpace = {
|
||||
if let sp = space {
|
||||
r = r + sp
|
||||
space = nil
|
||||
}
|
||||
}
|
||||
let resolved = colorMode.resolve(color)
|
||||
if showEdited, meta.itemEdited {
|
||||
r = r + statusIconText("pencil", resolved)
|
||||
r = r + statusIconText("pencil", color)
|
||||
}
|
||||
if meta.disappearing {
|
||||
r = r + statusIconText("timer", resolved).font(.caption2)
|
||||
r = r + statusIconText("timer", color).font(.caption2)
|
||||
let ttl = meta.itemTimed?.ttl
|
||||
if ttl != chatTTL {
|
||||
r = r + colored(Text(shortTimeText(ttl)), resolved)
|
||||
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
|
||||
}
|
||||
space = Text(" ")
|
||||
r = r + Text(" ")
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
appendSpace()
|
||||
r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
appendSpace()
|
||||
if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
|
||||
let metaColor = if onlyOverrides && statusColor == color {
|
||||
Color.clear
|
||||
} else {
|
||||
colorMode.resolve(statusColor)
|
||||
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
let gap = Text(" ").kerning(-1.25)
|
||||
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
|
||||
switch sent {
|
||||
case nil: r = r + t1
|
||||
case .sent: r = r + t1 + gap
|
||||
case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap
|
||||
case .rcvd2: r = r + gap + t1
|
||||
}
|
||||
r = r + colored(Text(image), metaColor)
|
||||
space = Text(" ")
|
||||
r = r + Text(" ")
|
||||
} else if !meta.disappearing {
|
||||
space = colorMode.statusSpacer + Text(" ")
|
||||
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
|
||||
}
|
||||
}
|
||||
if let enc = encrypted {
|
||||
appendSpace()
|
||||
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
|
||||
space = Text(" ")
|
||||
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
|
||||
}
|
||||
if showTimesamp {
|
||||
appendSpace()
|
||||
r = r + colored(meta.timestampText, resolved)
|
||||
r = r + meta.timestampText.foregroundColor(color)
|
||||
}
|
||||
return r.font(.caption)
|
||||
}
|
||||
|
||||
private func statusIconText(_ icon: String, _ color: Color?) -> Text {
|
||||
colored(Text(Image(systemName: icon)), color)
|
||||
}
|
||||
|
||||
// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
|
||||
private func colored(_ t: Text, _ color: Color?) -> Text {
|
||||
if let color {
|
||||
t.foregroundColor(color)
|
||||
} else {
|
||||
t
|
||||
}
|
||||
private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
Text(Image(systemName: icon)).foregroundColor(color)
|
||||
}
|
||||
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
|
||||
@@ -15,7 +15,7 @@ struct CIRcvDecryptionError: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.showTimestamp) var showTimestamp
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
var chatItem: ChatItem
|
||||
@@ -102,7 +102,8 @@ struct CIRcvDecryptionError: View {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
}
|
||||
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
}
|
||||
|
||||
private func basicDecryptionErrorItem() -> some View {
|
||||
@@ -126,7 +127,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
@@ -145,7 +146,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
|
||||
@@ -292,20 +292,28 @@ struct CIVideoView: View {
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private var fileSizeString: String {
|
||||
if let file = chatItem.file, !videoPlaying {
|
||||
" " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private func durationProgress() -> some View {
|
||||
Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
|
||||
.invertedForegroundStyle()
|
||||
HStack {
|
||||
Text("\(durationText(videoPlaying ? progress : duration))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding([.top, .leading], 6)
|
||||
|
||||
if let file = chatItem.file, !videoPlaying {
|
||||
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
@@ -403,9 +411,9 @@ struct CIVideoView: View {
|
||||
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.invertedForegroundStyle()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.white)
|
||||
.padding(smallView ? 0 : padding)
|
||||
}
|
||||
|
||||
@@ -420,8 +428,10 @@ struct CIVideoView: View {
|
||||
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
|
||||
Circle()
|
||||
.trim(from: 0, to: Double(progress) / Double(total))
|
||||
.stroke(style: StrokeStyle(lineWidth: 2))
|
||||
.invertedForegroundStyle()
|
||||
.stroke(
|
||||
Color(uiColor: .white),
|
||||
style: StrokeStyle(lineWidth: 2)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.frame(width: 16, height: 16)
|
||||
.padding([.trailing, .top], smallView ? 0 : 11)
|
||||
|
||||
@@ -12,7 +12,7 @@ import SimpleXChat
|
||||
struct FramedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
@@ -64,20 +64,16 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
|
||||
if let content = chatItem.content.msgContent {
|
||||
CIMetaView(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
metaColor: theme.colors.secondary,
|
||||
invertedMaterial: useWhiteMetaColor
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
if chatItem.content.msgContent != nil {
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
}
|
||||
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
@@ -190,7 +186,7 @@ struct FramedItemView: View {
|
||||
let v = ZStack(alignment: .topTrailing) {
|
||||
switch (qi.content) {
|
||||
case let .image(_, image):
|
||||
if let uiImage = imageFromBase64(image) {
|
||||
if let uiImage = UIImage(base64Encoded: image) {
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
|
||||
Image(uiImage: uiImage)
|
||||
@@ -202,7 +198,7 @@ struct FramedItemView: View {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
case let .video(_, image, _):
|
||||
if let uiImage = imageFromBase64(image) {
|
||||
if let uiImage = UIImage(base64Encoded: image) {
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
|
||||
Image(uiImage: uiImage)
|
||||
|
||||
@@ -13,7 +13,7 @@ import AVKit
|
||||
|
||||
struct FullScreenMediaView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@State var chatItem: ChatItem
|
||||
@State var image: UIImage?
|
||||
@State var player: AVPlayer? = nil
|
||||
|
||||
@@ -69,7 +69,8 @@ struct CIMsgError: View {
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ struct MarkedDeletedItemView: View {
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.modifier(ChatTailPadding(chatItem: chatItem))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
|
||||
|
||||
struct MsgContentView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.showTimestamp) var showTimestamp
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var text: String
|
||||
var formattedText: [FormattedText]? = nil
|
||||
@@ -85,7 +85,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -450,8 +450,20 @@ struct ChatItemInfoView: View {
|
||||
.foregroundColor(theme.colors.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
let (image, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
|
||||
image.foregroundColor(statusColor)
|
||||
let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
|
||||
switch status {
|
||||
case .rcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.padding(.trailing, 6)
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
}
|
||||
default:
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
}
|
||||
|
||||
if let (title, text) = status.statusInfo {
|
||||
|
||||
@@ -23,7 +23,7 @@ extension EnvironmentValues {
|
||||
struct ChatItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.showTimestamp) var showTimestamp
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@Binding var revealed: Bool
|
||||
@@ -72,7 +72,7 @@ struct ChatItemView: View {
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
.flatMap { imageFromBase64($0) }
|
||||
.flatMap { UIImage(base64Encoded: $0) }
|
||||
let adjustedMaxWidth = {
|
||||
if let preview, preview.size.width <= preview.size.height {
|
||||
maxWidth * 0.75
|
||||
|
||||
@@ -22,8 +22,8 @@ struct ChatView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State @ObservedObject var chat: Chat
|
||||
@StateObject private var scrollModel = ReverseListScrollModel()
|
||||
@StateObject private var floatingButtonModel: FloatingButtonModel = .shared
|
||||
@StateObject private var scrollModel = ReverseListScrollModel<ChatItem>()
|
||||
@StateObject private var floatingButtonModel = FloatingButtonModel()
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@@ -76,13 +76,7 @@ struct ChatView: View {
|
||||
VStack(spacing: 0) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
chatItemsList()
|
||||
// TODO: Extract into a separate view, to reduce the scope of `FloatingButtonModel` updates
|
||||
floatingButtons(
|
||||
unreadBelow: floatingButtonModel.unreadBelow,
|
||||
isNearBottom: floatingButtonModel.isNearBottom,
|
||||
date: floatingButtonModel.date,
|
||||
isDateVisible: floatingButtonModel.isDateVisible
|
||||
)
|
||||
floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
|
||||
}
|
||||
connectingText()
|
||||
if selectedChatItems == nil {
|
||||
@@ -419,6 +413,12 @@ struct ChatView: View {
|
||||
revealedChatItem: $revealedChatItem,
|
||||
selectedChatItems: $selectedChatItems
|
||||
)
|
||||
.onAppear {
|
||||
floatingButtonModel.appeared(viewId: ci.viewId)
|
||||
}
|
||||
.onDisappear {
|
||||
floatingButtonModel.disappeared(viewId: ci.viewId)
|
||||
}
|
||||
.id(ci.id) // Required to trigger `onAppear` on iOS15
|
||||
} loadPage: {
|
||||
loadChatItems(cInfo)
|
||||
@@ -429,10 +429,13 @@ struct ChatView: View {
|
||||
.onChange(of: searchText) { _ in
|
||||
Task { await loadChat(chat: chat, search: searchText) }
|
||||
}
|
||||
.onChange(of: im.reversedChatItems) { _ in
|
||||
floatingButtonModel.chatItemsChanged()
|
||||
}
|
||||
.onChange(of: im.itemAdded) { added in
|
||||
if added {
|
||||
im.itemAdded = false
|
||||
if floatingButtonModel.isReallyNearBottom {
|
||||
if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom {
|
||||
scrollModel.scrollToBottom()
|
||||
}
|
||||
}
|
||||
@@ -455,150 +458,95 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
class FloatingButtonModel: ObservableObject {
|
||||
static let shared = FloatingButtonModel()
|
||||
@Published var unreadBelow: Int = 0
|
||||
@Published var isNearBottom: Bool = true
|
||||
@Published var date: Date?
|
||||
@Published var isDateVisible: Bool = false
|
||||
var isReallyNearBottom: Bool { scrollOffset.value > 0 && scrollOffset.value < 500 }
|
||||
let listState = PassthroughSubject<ListState, Never>()
|
||||
let scrollOffset = CurrentValueSubject<Double, Never>(0)
|
||||
private enum Event {
|
||||
case appeared(String)
|
||||
case disappeared(String)
|
||||
case chatItemsChanged
|
||||
}
|
||||
|
||||
@Published var unreadChatItemCounts: UnreadChatItemCounts
|
||||
|
||||
private let events = PassthroughSubject<Event, Never>()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
|
||||
struct ListState: Equatable {
|
||||
let topItemDate: Date?
|
||||
let bottomItemId: ChatItem.ID?
|
||||
}
|
||||
|
||||
private struct ViewUpdate: Equatable {
|
||||
let unreadBelow: Int
|
||||
let date: Date?
|
||||
}
|
||||
|
||||
init() {
|
||||
// Date
|
||||
listState
|
||||
unreadChatItemCounts = UnreadChatItemCounts(
|
||||
isNearBottom: true,
|
||||
isReallyNearBottom: true,
|
||||
unreadBelow: 0
|
||||
)
|
||||
events
|
||||
.receive(on: DispatchQueue.global(qos: .background))
|
||||
.map { listState -> ViewUpdate in
|
||||
let im = ItemsModel.shared
|
||||
return ViewUpdate(
|
||||
unreadBelow: {
|
||||
if let id = listState.bottomItemId,
|
||||
let index = im.reversedChatItems.firstIndex(where: { $0.id == id }) {
|
||||
im.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
|
||||
if chatItem.isRcvNew { unread += 1 }
|
||||
}
|
||||
} else { 0 }
|
||||
}(),
|
||||
date: listState.topItemDate.map { Calendar.current.startOfDay(for: $0) }
|
||||
)
|
||||
}
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink {
|
||||
self.unreadBelow = $0.unreadBelow
|
||||
self.date = $0.date
|
||||
}
|
||||
.store(in: &bag)
|
||||
|
||||
// Date visibility
|
||||
listState
|
||||
.removeDuplicates()
|
||||
.map { _ in self.setDate(visibility: true) }
|
||||
// Hide the date after 1 second of no scrolling
|
||||
.debounce(for: 1, scheduler: DispatchQueue.main)
|
||||
.sink { self.setDate(visibility: false) }
|
||||
.store(in: &bag)
|
||||
|
||||
// Is near bottom
|
||||
scrollOffset
|
||||
.map { $0 < 800 }
|
||||
.removeDuplicates()
|
||||
// Delay the state change until scroll to bottom animation is finished
|
||||
.delay(for: 0.35, scheduler: DispatchQueue.main)
|
||||
.assign(to: \.isNearBottom, on: self)
|
||||
.store(in: &bag)
|
||||
}
|
||||
|
||||
func resetDate() {
|
||||
date = nil
|
||||
isDateVisible = false
|
||||
}
|
||||
|
||||
private func setDate(visibility isVisible: Bool) {
|
||||
Task {
|
||||
if isVisible {
|
||||
if !isNearBottom,
|
||||
!isDateVisible,
|
||||
let date, !Calendar.current.isDateInToday(date) {
|
||||
await MainActor.run {
|
||||
isDateVisible = true
|
||||
}
|
||||
}
|
||||
} else if isDateVisible {
|
||||
await MainActor.run {
|
||||
isDateVisible = false
|
||||
.scan(Set<String>()) { itemsInView, event in
|
||||
var updated = itemsInView
|
||||
switch event {
|
||||
case let .appeared(viewId): updated.insert(viewId)
|
||||
case let .disappeared(viewId): updated.remove(viewId)
|
||||
case .chatItemsChanged: ()
|
||||
}
|
||||
return updated
|
||||
}
|
||||
}
|
||||
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) }
|
||||
.removeDuplicates()
|
||||
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
|
||||
.assign(to: \.unreadChatItemCounts, on: self)
|
||||
.store(in: &bag)
|
||||
}
|
||||
|
||||
func appeared(viewId: String) {
|
||||
events.send(.appeared(viewId))
|
||||
}
|
||||
|
||||
func disappeared(viewId: String) {
|
||||
events.send(.disappeared(viewId))
|
||||
}
|
||||
|
||||
func chatItemsChanged() {
|
||||
events.send(.chatItemsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func floatingButtons(unreadBelow: Int, isNearBottom: Bool, date: Date?, isDateVisible: Bool) -> some View {
|
||||
ZStack(alignment: .top) {
|
||||
if let date {
|
||||
DateSeparator(date: date)
|
||||
.padding(.vertical, 4).padding(.horizontal, 8)
|
||||
.background(.thinMaterial)
|
||||
.clipShape(Capsule())
|
||||
.opacity(isDateVisible ? 1 : 0)
|
||||
}
|
||||
VStack {
|
||||
let unreadAbove = chat.chatStats.unreadCount - unreadBelow
|
||||
if unreadAbove > 0 {
|
||||
circleButton {
|
||||
unreadCountText(unreadAbove)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToNextPage()
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
Task {
|
||||
await markChatRead(chat)
|
||||
}
|
||||
} label: {
|
||||
Label("Mark read", systemImage: "checkmark")
|
||||
|
||||
private func floatingButtons(counts: UnreadChatItemCounts) -> some View {
|
||||
VStack {
|
||||
let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow
|
||||
if unreadAbove > 0 {
|
||||
circleButton {
|
||||
unreadCountText(unreadAbove)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToNextPage()
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
Task {
|
||||
await markChatRead(chat)
|
||||
}
|
||||
} label: {
|
||||
Label("Mark read", systemImage: "checkmark")
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if unreadBelow > 0 {
|
||||
circleButton {
|
||||
unreadCountText(unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToBottom()
|
||||
}
|
||||
} else if !isNearBottom {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollModel.scrollToBottom() }
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
Spacer()
|
||||
if counts.unreadBelow > 0 {
|
||||
circleButton {
|
||||
unreadCountText(counts.unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToBottom()
|
||||
}
|
||||
} else if !counts.isNearBottom {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollModel.scrollToBottom() }
|
||||
}
|
||||
}
|
||||
.onDisappear(perform: floatingButtonModel.resetDate)
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
|
||||
@@ -610,21 +558,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DateSeparator: View {
|
||||
let date: Date
|
||||
|
||||
var body: some View {
|
||||
Text(String.localizedStringWithFormat(
|
||||
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
|
||||
date.formatted(.dateTime.weekday(.abbreviated)),
|
||||
date.formatted(.dateTime.day().month(.abbreviated))
|
||||
))
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, media)
|
||||
@@ -635,8 +568,8 @@ struct ChatView: View {
|
||||
|
||||
private func endCallButton(_ call: Call) -> some View {
|
||||
Button {
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID {
|
||||
CallController.shared.endCall(callUUID: callUUID)
|
||||
if let uuid = call.callkitUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
} else {
|
||||
CallController.shared.endCall(call: call) {}
|
||||
}
|
||||
@@ -777,37 +710,38 @@ struct ChatView: View {
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showForwardingSheet: Bool = false
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
var revealed: Bool { chatItem == revealedChatItem }
|
||||
|
||||
typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?)
|
||||
struct TimeSeparation {
|
||||
let isTimestampShown: Bool
|
||||
let isGapLarge: Bool
|
||||
|
||||
func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation {
|
||||
let im = ItemsModel.shared
|
||||
if let i, i > 0 && im.reversedChatItems.count >= i {
|
||||
let nextItem = im.reversedChatItems[i - 1]
|
||||
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
|
||||
return (
|
||||
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
|
||||
largeGap: largeGap,
|
||||
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
|
||||
)
|
||||
} else {
|
||||
return (timestamp: true, largeGap: true, date: nil)
|
||||
init(for chatItem: ChatItem, at index: Int?) {
|
||||
let im = ItemsModel.shared
|
||||
if let index, index > 0 && !im.reversedChatItems.isEmpty {
|
||||
let nextItem = im.reversedChatItems[index - 1]
|
||||
isGapLarge = nextItem.chatDir != chatItem.chatDir ||
|
||||
nextItem.meta.createdAt.timeIntervalSince(chatItem.meta.createdAt) > 30
|
||||
isTimestampShown = isGapLarge ||
|
||||
formatTimestampText(chatItem.meta.createdAt) != formatTimestampText(nextItem.meta.createdAt)
|
||||
} else {
|
||||
isTimestampShown = true
|
||||
isGapLarge = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let currIndex = m.getChatItemIndex(chatItem)
|
||||
let (currIndex, _) = m.getNextChatItem(chatItem)
|
||||
let ciCategory = chatItem.mergeCategory
|
||||
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||
let range = itemsRange(currIndex, prevHidden)
|
||||
let timeSeparation = getItemSeparation(chatItem, at: currIndex)
|
||||
let timeSeparation = TimeSeparation(for: chatItem, at: currIndex)
|
||||
let im = ItemsModel.shared
|
||||
Group {
|
||||
if revealed, let range = range {
|
||||
@@ -815,7 +749,7 @@ struct ChatView: View {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
|
||||
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
|
||||
chatItemView(ci, nil, prev, getItemSeparation(ci, at: i))
|
||||
chatItemView(ci, nil, prev, TimeSeparation(for: ci, at: i))
|
||||
.overlay {
|
||||
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
|
||||
Color.clear
|
||||
@@ -829,12 +763,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
chatItemView(chatItem, range, prevItem, timeSeparation)
|
||||
if let date = timeSeparation.date {
|
||||
DateSeparator(date: date).padding(8)
|
||||
}
|
||||
}
|
||||
chatItemView(chatItem, range, prevItem, timeSeparation)
|
||||
.overlay {
|
||||
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
||||
Color.clear
|
||||
@@ -884,54 +813,9 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct MemberLayout: Layout {
|
||||
let spacing: Double
|
||||
let msgWidth: Double
|
||||
|
||||
private func sizes(subviews: Subviews, proposal: ProposedViewSize) -> (CGSize, CGSize) {
|
||||
assert(subviews.count == 2, "member layout must contain exactly two subviews")
|
||||
let roleSize = subviews[1].sizeThatFits(proposal)
|
||||
let memberSize = subviews[0].sizeThatFits(
|
||||
ProposedViewSize(
|
||||
width: (proposal.width ?? msgWidth) - roleSize.width,
|
||||
height: proposal.height
|
||||
)
|
||||
)
|
||||
return (memberSize, roleSize)
|
||||
}
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
||||
let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
|
||||
return CGSize(
|
||||
width: min(
|
||||
proposal.width ?? msgWidth,
|
||||
max(msgWidth, roleSize.width + spacing + memberSize.width)
|
||||
),
|
||||
height: max(memberSize.height, roleSize.height)
|
||||
)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
||||
let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
|
||||
subviews[0].place(
|
||||
at: CGPoint(x: bounds.minX, y: bounds.midY - memberSize.height / 2),
|
||||
proposal: ProposedViewSize(memberSize)
|
||||
)
|
||||
subviews[1].place(
|
||||
at: CGPoint(
|
||||
x: bounds.minX + max(memberSize.width + spacing, msgWidth - roleSize.width),
|
||||
y: bounds.midY - roleSize.height / 2
|
||||
),
|
||||
proposal: ProposedViewSize(roleSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View {
|
||||
let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
|
||||
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ timeSeparation: TimeSeparation) -> some View {
|
||||
let bottomPadding: Double = timeSeparation.isGapLarge ? 6 : 2
|
||||
if case let .groupRcv(member) = ci.chatDir,
|
||||
case let .group(groupInfo) = chat.chatInfo {
|
||||
let (prevMember, memCount): (GroupMember?, Int) =
|
||||
@@ -943,49 +827,24 @@ struct ChatView: View {
|
||||
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
Group {
|
||||
if memCount == 1 && member.memberRole > .member {
|
||||
Group {
|
||||
if #available(iOS 16.0, *) {
|
||||
MemberLayout(spacing: 16, msgWidth: msgWidth) {
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Text(member.memberRole.text)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 16) {
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Text(member.memberRole.text)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
maxWidth: maxWidth,
|
||||
alignment: chatItem.chatDir.sent ? .trailing : .leading
|
||||
)
|
||||
} else {
|
||||
Text(memberNames(member, prevMember, memCount))
|
||||
.lineLimit(2)
|
||||
}
|
||||
let t = if memCount == 1 && member.memberRole > .member {
|
||||
Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName)
|
||||
} else {
|
||||
Text(memberNames(member, prevMember, memCount))
|
||||
}
|
||||
t
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0))
|
||||
.padding(.top, 3) // this is in addition to message sequence gap
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
if selectedChatItems != nil && ci.canBeDeletedForSelf {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if let member = m.getGroupMember(member.groupMemberId) {
|
||||
@@ -998,8 +857,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,9 +870,9 @@ struct ChatView: View {
|
||||
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
|
||||
.padding(.leading, 12)
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
.padding(.leading, memberImageSize + 8 + 12)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
@@ -1029,7 +887,7 @@ struct ChatView: View {
|
||||
.padding(.leading)
|
||||
}
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
@@ -1047,7 +905,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
|
||||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ timeSeparation: TimeSeparation) -> some View {
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
ChatItemView(
|
||||
@@ -1057,8 +915,8 @@ struct ChatView: View {
|
||||
revealed: .constant(revealed),
|
||||
allowMenu: $allowMenu
|
||||
)
|
||||
.environment(\.showTimestamp, itemSeparation.timestamp)
|
||||
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap))
|
||||
.environment(\.showTimestamp, timeSeparation.isTimestampShown)
|
||||
.modifier(ChatItemClipped(ci, isTailVisible: timeSeparation.isGapLarge))
|
||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
|
||||
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
let imgs: [UIImage] = images.compactMap { image in
|
||||
imageFromBase64(image)
|
||||
UIImage(base64Encoded: image)
|
||||
}
|
||||
if imgs.count == 0 {
|
||||
ProgressView()
|
||||
|
||||
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
|
||||
|
||||
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import SimpleXChat
|
||||
|
||||
/// A List, which displays it's items in reverse order - from bottom to top
|
||||
struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
let items: Array<ChatItem>
|
||||
struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
|
||||
let items: Array<Item>
|
||||
|
||||
@Binding var scrollState: ReverseListScrollModel.State
|
||||
@Binding var scrollState: ReverseListScrollModel<Item>.State
|
||||
|
||||
/// Closure, that returns user interface for a given item
|
||||
let content: (ChatItem) -> Content
|
||||
let content: (Item) -> Content
|
||||
|
||||
let loadPage: () -> Void
|
||||
|
||||
@@ -26,9 +25,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: Controller, context: Context) {
|
||||
controller.representer = self
|
||||
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
|
||||
controller.view.layer.removeAllAnimations()
|
||||
switch destination {
|
||||
case .nextPage:
|
||||
controller.scrollToNextPage()
|
||||
@@ -45,10 +42,9 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
/// Controller, which hosts SwiftUI cells
|
||||
class Controller: UITableViewController {
|
||||
private enum Section { case main }
|
||||
var representer: ReverseList
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
|
||||
private let representer: ReverseList
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
private var itemCount: Int = 0
|
||||
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
|
||||
init(representer: ReverseList) {
|
||||
@@ -75,7 +71,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
// 3. Configure data source
|
||||
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
|
||||
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
|
||||
tableView: tableView
|
||||
) { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
|
||||
@@ -107,10 +103,6 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
name: notificationName,
|
||||
object: nil
|
||||
)
|
||||
updateFloatingButtons
|
||||
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { self.updateVisibleItems() }
|
||||
.store(in: &bag)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@@ -179,8 +171,8 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
|
||||
func update(items: [ChatItem]) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
|
||||
func update(items: Array<Item>) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.defaultRowAnimation = .none
|
||||
@@ -196,39 +188,6 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
)
|
||||
}
|
||||
itemCount = items.count
|
||||
updateFloatingButtons.send()
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
updateFloatingButtons.send()
|
||||
}
|
||||
|
||||
private func updateVisibleItems() {
|
||||
if let visibleRows = tableView.indexPathsForVisibleRows,
|
||||
visibleRows.last?.item ?? 0 < representer.items.count {
|
||||
let fbm = ChatView.FloatingButtonModel.shared
|
||||
fbm.scrollOffset.send(tableView.contentOffset.y + InvertedTableView.inset)
|
||||
fbm.listState.send(.init(
|
||||
topItemDate: visibleRows
|
||||
.last { isVisible(indexPath: $0) }
|
||||
.map { representer.items[$0.item] }?
|
||||
.meta.itemTs,
|
||||
bottomItemId: visibleRows
|
||||
.first { isVisible(indexPath: $0) }
|
||||
.map { representer.items[$0.item] }?
|
||||
.id
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func isVisible(indexPath: IndexPath) -> Bool {
|
||||
if let relativeFrame = tableView.superview?.convert(
|
||||
tableView.rectForRow(at: indexPath),
|
||||
from: tableView
|
||||
) {
|
||||
relativeFrame.maxY > InvertedTableView.inset &&
|
||||
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
|
||||
} else { false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,12 +232,12 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
/// Manages ``ReverseList`` scrolling
|
||||
class ReverseListScrollModel: ObservableObject {
|
||||
class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
|
||||
/// Represents Scroll State of ``ReverseList``
|
||||
enum State: Equatable {
|
||||
enum Destination: Equatable {
|
||||
case nextPage
|
||||
case item(ChatItem.ID)
|
||||
case item(Item.ID)
|
||||
case bottom
|
||||
}
|
||||
|
||||
@@ -296,7 +255,7 @@ class ReverseListScrollModel: ObservableObject {
|
||||
state = .scrollingTo(.bottom)
|
||||
}
|
||||
|
||||
func scrollToItem(id: ChatItem.ID) {
|
||||
func scrollToItem(id: Item.ID) {
|
||||
state = .scrollingTo(.item(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ struct ChatPreviewView: View {
|
||||
case let .link(_, preview):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
|
||||
@@ -323,13 +323,13 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case let .image(_, image):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel())
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
}
|
||||
case let .video(_,image, duration):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel())
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
}
|
||||
case let .voice(_, duration):
|
||||
smallContentPreviewVoice(size: dynamicMediaSize) {
|
||||
|
||||
@@ -15,144 +15,136 @@ import SimpleXChat
|
||||
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
|
||||
struct ChatItemClipped: ViewModifier {
|
||||
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
|
||||
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
|
||||
private let chatItem: (content: CIContent, chatDir: CIDirection)?
|
||||
private let tailVisible: Bool
|
||||
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var isTailEnabled = true
|
||||
|
||||
let chatItem: ChatItem?
|
||||
let isTailVisible: Bool
|
||||
|
||||
init() {
|
||||
self.chatItem = nil
|
||||
self.tailVisible = false
|
||||
chatItem = nil
|
||||
isTailVisible = false
|
||||
}
|
||||
|
||||
init(_ ci: ChatItem, tailVisible: Bool) {
|
||||
self.chatItem = (ci.content, ci.chatDir)
|
||||
self.tailVisible = tailVisible
|
||||
init(_ chatItem: ChatItem, isTailVisible: Bool) {
|
||||
self.chatItem = chatItem
|
||||
self.isTailVisible = isTailVisible
|
||||
}
|
||||
|
||||
private func shapeStyle() -> ChatItemShape.Style {
|
||||
if let ci = chatItem {
|
||||
switch ci.content {
|
||||
|
||||
fileprivate func itemShape() -> ChatItemShape {
|
||||
if let chatItem {
|
||||
switch chatItem.content {
|
||||
case
|
||||
.sndMsgContent,
|
||||
.rcvMsgContent,
|
||||
.rcvDecryptionError,
|
||||
.sndDeleted,
|
||||
.rcvDeleted,
|
||||
.rcvIntegrityError,
|
||||
.sndModerated,
|
||||
.rcvModerated,
|
||||
.rcvBlocked,
|
||||
.invalidJSON:
|
||||
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
|
||||
false
|
||||
} else {
|
||||
tailVisible
|
||||
}
|
||||
return tailEnabled
|
||||
.sndMsgContent,
|
||||
.rcvMsgContent,
|
||||
.rcvDecryptionError,
|
||||
.rcvGroupInvitation,
|
||||
.sndGroupInvitation,
|
||||
.sndDeleted,
|
||||
.rcvDeleted,
|
||||
.rcvIntegrityError,
|
||||
.sndModerated,
|
||||
.rcvModerated,
|
||||
.rcvBlocked,
|
||||
.invalidJSON: isTailEnabled
|
||||
? .bubble(
|
||||
padding: ci.chatDir.sent ? .trailing : .leading,
|
||||
tailVisible: tail
|
||||
padding: chatItem.chatDir.sent ? .trailing : .leading,
|
||||
isTailVisible: Self.hidesTail(chatItem.content.msgContent)
|
||||
? false
|
||||
: isTailVisible
|
||||
)
|
||||
: .roundRect(radius: msgRectMaxRadius)
|
||||
case .rcvGroupInvitation, .sndGroupInvitation:
|
||||
return .roundRect(radius: msgRectMaxRadius)
|
||||
default: return .roundRect(radius: 8)
|
||||
: .roundRect(maxRadius: ChatBubble.maxRadius)
|
||||
default: .roundRect(maxRadius: 8)
|
||||
}
|
||||
} else {
|
||||
return .roundRect(radius: msgRectMaxRadius)
|
||||
.roundRect(maxRadius: 8)
|
||||
}
|
||||
}
|
||||
|
||||
// Tail is hidden for images and video without any text
|
||||
private static func hidesTail(_ msgContent: MsgContent?) -> Bool {
|
||||
if let msgContent, msgContent.isImageOrVideo && msgContent.text.isEmpty {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let clipShape = ChatItemShape(
|
||||
roundness: roundness,
|
||||
style: shapeStyle()
|
||||
)
|
||||
let shape = ChatBubble(roundness: roundness, shapePath: itemShape())
|
||||
content
|
||||
.contentShape(.dragPreview, clipShape)
|
||||
.contentShape(.contextMenuPreview, clipShape)
|
||||
.clipShape(clipShape)
|
||||
.contentShape(.dragPreview, shape)
|
||||
.contentShape(.contextMenuPreview, shape)
|
||||
.clipShape(shape)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatTailPadding: ViewModifier {
|
||||
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
|
||||
let chatItem: ChatItem
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.padding(.horizontal, -msgTailWidth)
|
||||
if tailEnabled {
|
||||
content.padding(
|
||||
chatItem.chatDir.sent ? .trailing : .leading,
|
||||
ChatBubble.tailSize
|
||||
)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let msgRectMaxRadius: Double = 18
|
||||
private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2
|
||||
private let msgTailWidth: Double = 9
|
||||
private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg
|
||||
private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg
|
||||
|
||||
struct ChatItemShape: Shape {
|
||||
fileprivate enum Style {
|
||||
case bubble(padding: HorizontalEdge, tailVisible: Bool)
|
||||
case roundRect(radius: Double)
|
||||
}
|
||||
|
||||
struct ChatBubble: Shape {
|
||||
static let tailSize: Double = 8
|
||||
static let maxRadius: Double = 16
|
||||
fileprivate let roundness: Double
|
||||
fileprivate let style: Style
|
||||
fileprivate let shapePath: ChatItemShape
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
switch style {
|
||||
case let .bubble(padding, tailVisible):
|
||||
let w = rect.width
|
||||
let h = rect.height
|
||||
let rxMax = min(msgBubbleMaxRadius, w / 2)
|
||||
let ryMax = min(msgBubbleMaxRadius, h / 2)
|
||||
let rx = roundness * rxMax
|
||||
let ry = roundness * ryMax
|
||||
let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2)
|
||||
switch shapePath {
|
||||
case .bubble(let padding, let isTailVisible):
|
||||
let rMax = min(Self.maxRadius, min(rect.width, rect.height) / 2)
|
||||
let r = roundness * rMax
|
||||
let tailHeight = rect.height - (Self.tailSize + (rMax - Self.tailSize) * roundness)
|
||||
var path = Path()
|
||||
// top side
|
||||
path.move(to: CGPoint(x: rx, y: 0))
|
||||
path.addLine(to: CGPoint(x: w - rx, y: 0))
|
||||
if roundness > 0 {
|
||||
// top-right corner
|
||||
path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0))
|
||||
}
|
||||
if rect.height > 2 * ry {
|
||||
// right side
|
||||
path.addLine(to: CGPoint(x: w, y: h - ry))
|
||||
}
|
||||
if roundness > 0 {
|
||||
// bottom-right corner
|
||||
path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h))
|
||||
}
|
||||
// bottom side
|
||||
if tailVisible {
|
||||
path.addLine(to: CGPoint(x: -msgTailWidth, y: h))
|
||||
if roundness > 0 {
|
||||
// bottom-left tail
|
||||
// distance of control point from touch point, calculated via ratios
|
||||
let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
|
||||
// tail control point
|
||||
let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness))
|
||||
// bottom-left tail curve
|
||||
path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc)
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: 0, y: h - tailHeight))
|
||||
}
|
||||
if rect.height > ry + tailHeight {
|
||||
// left side
|
||||
path.addLine(to: CGPoint(x: 0, y: ry))
|
||||
}
|
||||
path.addArc(
|
||||
center: CGPoint(x: r + Self.tailSize, y: r),
|
||||
radius: r,
|
||||
startAngle: .degrees(270),
|
||||
endAngle: .degrees(180),
|
||||
clockwise: true
|
||||
)
|
||||
if isTailVisible {
|
||||
path.addLine(
|
||||
to: CGPoint(x: Self.tailSize, y: tailHeight)
|
||||
)
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: rect.height),
|
||||
control: CGPoint(x: Self.tailSize, y: tailHeight + r * 0.64)
|
||||
)
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: rx, y: h))
|
||||
path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h))
|
||||
if rect.height > 2 * ry {
|
||||
// left side
|
||||
path.addLine(to: CGPoint(x: 0, y: ry))
|
||||
}
|
||||
path.addArc(
|
||||
center: CGPoint(x: r + Self.tailSize, y: rect.height - r),
|
||||
radius: r,
|
||||
startAngle: .degrees(180),
|
||||
endAngle: .degrees(90),
|
||||
clockwise: true
|
||||
)
|
||||
}
|
||||
if roundness > 0 {
|
||||
// top-left corner
|
||||
path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0))
|
||||
}
|
||||
path.closeSubpath()
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.width - r, y: rect.height - r),
|
||||
radius: r,
|
||||
startAngle: .degrees(90),
|
||||
endAngle: .degrees(0),
|
||||
clockwise: true
|
||||
)
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.width - r, y: r),
|
||||
radius: r,
|
||||
startAngle: .degrees(0),
|
||||
endAngle: .degrees(270),
|
||||
clockwise: true
|
||||
)
|
||||
return switch padding {
|
||||
case .leading: path
|
||||
case .trailing: path
|
||||
@@ -163,18 +155,9 @@ struct ChatItemShape: Shape {
|
||||
return Path(roundedRect: rect, cornerRadius: radius * roundness)
|
||||
}
|
||||
}
|
||||
|
||||
var offset: Double? {
|
||||
switch style {
|
||||
case let .bubble(padding, isTailVisible):
|
||||
if isTailVisible {
|
||||
switch padding {
|
||||
case .leading: -msgTailWidth
|
||||
case .trailing: msgTailWidth
|
||||
}
|
||||
} else { 0 }
|
||||
case .roundRect: 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate enum ChatItemShape {
|
||||
case bubble(padding: HorizontalEdge, isTailVisible: Bool)
|
||||
case roundRect(maxRadius: Double)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Test.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 31/08/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func invertedForegroundStyle(enabled: Bool = true) -> some View {
|
||||
if enabled {
|
||||
foregroundStyle(Material.ultraThin)
|
||||
.environment(\.colorScheme, .dark)
|
||||
.grayscale(1)
|
||||
.contrast(-20)
|
||||
} else { self }
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ struct ProfileImage: View {
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
if let uiImage = imageFromBase64(imageStr) {
|
||||
if let uiImage = UIImage(base64Encoded: imageStr) {
|
||||
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
|
||||
} else {
|
||||
let c = color.asAnotherColorFromSecondaryVariant(theme)
|
||||
|
||||
@@ -11,18 +11,12 @@ import SwiftUI
|
||||
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
|
||||
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
|
||||
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
|
||||
let rootViewController = keyWindow.rootViewController {
|
||||
// Find the top-most presented view controller
|
||||
var topController = rootViewController
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
|
||||
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
if let completed = completed {
|
||||
activityViewController.completionWithItemsHandler = { _, _, _, _ in
|
||||
completed()
|
||||
}
|
||||
}
|
||||
topController.present(activityViewController, animated: true)
|
||||
let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
|
||||
activityViewController.completionWithItemsHandler = handler
|
||||
}
|
||||
presentedViewController.present(activityViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func chatContactType(chat: Chat) -> ContactType {
|
||||
case .contactRequest:
|
||||
return .request
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
return .card
|
||||
} else if contact.chatDeleted {
|
||||
return .chatDeleted
|
||||
|
||||
@@ -327,12 +327,6 @@ private struct InviteView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private enum ProfileSwitchStatus {
|
||||
case switchingUser
|
||||
case switchingIncognito
|
||||
case idle
|
||||
}
|
||||
|
||||
private struct ActiveProfilePicker: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -342,7 +336,7 @@ private struct ActiveProfilePicker: View {
|
||||
@Binding var incognitoEnabled: Bool
|
||||
@Binding var choosingProfile: Bool
|
||||
@State private var alert: SomeAlert?
|
||||
@State private var profileSwitchStatus: ProfileSwitchStatus = .idle
|
||||
@State private var switchingProfile = false
|
||||
@State private var switchingProfileByTimeout = false
|
||||
@State private var lastSwitchingProfileByTimeoutCall: Double?
|
||||
@State private var profiles: [User] = []
|
||||
@@ -364,7 +358,7 @@ private struct ActiveProfilePicker: View {
|
||||
.sorted { u, _ in u.activeUser }
|
||||
}
|
||||
.onChange(of: incognitoEnabled) { incognito in
|
||||
if profileSwitchStatus != .switchingIncognito {
|
||||
if !switchingProfile {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -375,12 +369,12 @@ private struct ActiveProfilePicker: View {
|
||||
await MainActor.run {
|
||||
contactConnection = conn
|
||||
chatModel.updateContactConnection(conn)
|
||||
profileSwitchStatus = .idle
|
||||
switchingProfile = false
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
profileSwitchStatus = .idle
|
||||
switchingProfile = false
|
||||
incognitoEnabled = !incognito
|
||||
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
|
||||
let err = getErrorAlert(error, "Error changing to incognito!")
|
||||
@@ -395,39 +389,39 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: profileSwitchStatus) { sp in
|
||||
if sp != .idle {
|
||||
.onChange(of: switchingProfile) { sp in
|
||||
if sp {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
switchingProfileByTimeout = profileSwitchStatus != .idle
|
||||
switchingProfileByTimeout = switchingProfile
|
||||
}
|
||||
} else {
|
||||
switchingProfileByTimeout = false
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedProfile) { profile in
|
||||
if (profileSwitchStatus != .switchingUser) {
|
||||
if (profile == chatModel.currentUser) {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
switchingProfile = true
|
||||
if let contactConn = contactConnection,
|
||||
let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) {
|
||||
|
||||
await MainActor.run {
|
||||
contactConnection = conn
|
||||
connReqInvitation = conn.connReqInv ?? ""
|
||||
incognitoEnabled = false
|
||||
chatModel.updateContactConnection(conn)
|
||||
}
|
||||
do {
|
||||
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
switchingProfile = false
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
switchingProfile = false
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Error switching profile"),
|
||||
@@ -440,7 +434,7 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
switchingProfile = false
|
||||
if let currentUser = chatModel.currentUser {
|
||||
selectedProfile = currentUser
|
||||
}
|
||||
@@ -499,12 +493,10 @@ private struct ActiveProfilePicker: View {
|
||||
|
||||
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
Button {
|
||||
if selectedProfile == user && incognitoEnabled {
|
||||
if selectedProfile != user || incognitoEnabled {
|
||||
switchingProfile = true
|
||||
incognitoEnabled = false
|
||||
profileSwitchStatus = .switchingIncognito
|
||||
} else if selectedProfile != user {
|
||||
selectedProfile = user
|
||||
profileSwitchStatus = .switchingUser
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
@@ -527,7 +519,7 @@ private struct ActiveProfilePicker: View {
|
||||
let incognitoOption = Button {
|
||||
if !incognitoEnabled {
|
||||
incognitoEnabled = true
|
||||
profileSwitchStatus = .switchingIncognito
|
||||
switchingProfile = true
|
||||
}
|
||||
} label : {
|
||||
HStack {
|
||||
|
||||
@@ -181,10 +181,10 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) {
|
||||
Section(header: Text("Messages").foregroundColor(theme.colors.secondary)) {
|
||||
HStack {
|
||||
Text("Corner")
|
||||
Slider(value: $chatItemRoundness, in: 0...1, step: 0.05)
|
||||
Text("Roundness")
|
||||
Slider(value: $chatItemRoundness, in: 0...1, step: 0.1)
|
||||
}
|
||||
Toggle("Tail", isOn: $chatItemTail)
|
||||
}
|
||||
@@ -368,21 +368,20 @@ struct ChatThemePreview: View {
|
||||
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
||||
HStack {
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
|
||||
.modifier(ChatItemClipped(alice, tailVisible: true))
|
||||
.modifier(ChatItemClipped(alice, isTailVisible: true))
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
|
||||
.modifier(ChatItemClipped(bob, tailVisible: true))
|
||||
.modifier(ChatItemClipped(bob, isTailVisible: true))
|
||||
.frame(alignment: .trailing)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor {
|
||||
|
||||
@@ -77,8 +77,6 @@ let DEFAULT_THEME_OVERRIDES = "themeOverrides"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
let defaultChatItemRoundness: Double = 0.75
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
DEFAULT_LA_NOTICE_SHOWN: false,
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ иска да се свърже!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ и %lld членове</target>
|
||||
@@ -1689,10 +1685,6 @@ This is your own one-time link!</source>
|
||||
<target>Версия на ядрото: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Поправи име на %@?</target>
|
||||
@@ -2653,10 +2645,6 @@ This is your own one-time link!</source>
|
||||
<target>Грешка при промяна на адреса</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Грешка при промяна на ролята</target>
|
||||
@@ -2667,10 +2655,6 @@ This is your own one-time link!</source>
|
||||
<target>Грешка при промяна на настройката</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2886,10 +2870,6 @@ This is your own one-time link!</source>
|
||||
<target>Грешка при спиране на чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Грешка при смяна на профил!</target>
|
||||
@@ -4089,10 +4069,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Източникът на съобщението остава скрит.</target>
|
||||
@@ -5586,10 +5562,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Избери</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5939,10 +5911,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Сподели линк</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Сподели този еднократен линк за връзка</target>
|
||||
@@ -6267,10 +6235,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Направи снимка</target>
|
||||
@@ -7516,10 +7480,6 @@ Repeat connection request?</source>
|
||||
<target>Вашите чат профили</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).</target>
|
||||
|
||||
@@ -135,10 +135,6 @@
|
||||
<target>%@ se chce připojit!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1627,10 +1623,6 @@ This is your own one-time link!</source>
|
||||
<target>Verze jádra: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2560,10 +2552,6 @@ This is your own one-time link!</source>
|
||||
<target>Chuba změny adresy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Chyba při změně role</target>
|
||||
@@ -2574,10 +2562,6 @@ This is your own one-time link!</source>
|
||||
<target>Chyba změny nastavení</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2788,10 +2772,6 @@ This is your own one-time link!</source>
|
||||
<target>Chyba při zastavení chatu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Chyba při přepínání profilu!</target>
|
||||
@@ -3948,10 +3928,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5387,10 +5363,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Vybrat</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5736,10 +5708,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Sdílet odkaz</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -6056,10 +6024,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Vyfotit</target>
|
||||
@@ -7245,10 +7209,6 @@ Repeat connection request?</source>
|
||||
<target>Vaše chat profily</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ will sich mit Ihnen verbinden!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ und %lld Mitglieder</target>
|
||||
@@ -1744,10 +1740,6 @@ Das ist Ihr eigener Einmal-Link!</target>
|
||||
<target>Core Version: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Richtiger Name für %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ Das ist Ihr eigener Einmal-Link!</target>
|
||||
<target>Fehler beim Wechseln der Empfängeradresse</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Fehler beim Ändern der Rolle</target>
|
||||
@@ -2746,10 +2734,6 @@ Das ist Ihr eigener Einmal-Link!</target>
|
||||
<target>Fehler beim Ändern der Einstellung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut.</target>
|
||||
@@ -2970,10 +2954,6 @@ Das ist Ihr eigener Einmal-Link!</target>
|
||||
<target>Fehler beim Beenden des Chats</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Fehler beim Umschalten des Profils!</target>
|
||||
@@ -4204,10 +4184,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
||||
<target>Nachrichten-Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Die Nachrichtenquelle bleibt privat.</target>
|
||||
@@ -5753,10 +5729,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
||||
<target>Auswählen</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>%lld ausgewählt</target>
|
||||
@@ -6127,10 +6099,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
||||
<target>Link teilen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Teilen Sie diesen Einmal-Einladungslink</target>
|
||||
@@ -6471,10 +6439,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Machen Sie ein Foto</target>
|
||||
@@ -7755,10 +7719,6 @@ Verbindungsanfrage wiederholen?</target>
|
||||
<target>Ihre Chat-Profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@).</target>
|
||||
|
||||
@@ -137,11 +137,6 @@
|
||||
<target>%@ wants to connect!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<target>%1$@, %2$@</target>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ and %lld members</target>
|
||||
@@ -1745,11 +1740,6 @@ This is your own one-time link!</target>
|
||||
<target>Core version: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<target>Corner</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Correct name to %@?</target>
|
||||
@@ -2734,11 +2724,6 @@ This is your own one-time link!</target>
|
||||
<target>Error changing address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<target>Error changing connection profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Error changing role</target>
|
||||
@@ -2749,11 +2734,6 @@ This is your own one-time link!</target>
|
||||
<target>Error changing setting</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<target>Error changing to incognito!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Error connecting to forwarding server %@. Please try later.</target>
|
||||
@@ -2974,11 +2954,6 @@ This is your own one-time link!</target>
|
||||
<target>Error stopping chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<target>Error switching profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Error switching profile!</target>
|
||||
@@ -4209,11 +4184,6 @@ This is your link for group %@!</target>
|
||||
<target>Message servers</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<target>Message shape</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Message source remains private.</target>
|
||||
@@ -5759,11 +5729,6 @@ Enable in *Network & servers* settings.</target>
|
||||
<target>Select</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<target>Select chat profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Selected %lld</target>
|
||||
@@ -6134,11 +6099,6 @@ Enable in *Network & servers* settings.</target>
|
||||
<target>Share link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<target>Share profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Share this 1-time invite link</target>
|
||||
@@ -6479,11 +6439,6 @@ Enable in *Network & servers* settings.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<target>Tail</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Take picture</target>
|
||||
@@ -7764,11 +7719,6 @@ Repeat connection request?</target>
|
||||
<target>Your chat profiles</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<target>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Your contact sent a file that is larger than currently supported maximum size (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>¡ %@ quiere contactar!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ y %lld miembro(s) más</target>
|
||||
@@ -1744,10 +1740,6 @@ This is your own one-time link!</source>
|
||||
<target>Versión Core: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>¿Corregir el nombre a %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ This is your own one-time link!</source>
|
||||
<target>Error al cambiar servidor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Error al cambiar rol</target>
|
||||
@@ -2746,10 +2734,6 @@ This is your own one-time link!</source>
|
||||
<target>Error cambiando configuración</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde.</target>
|
||||
@@ -2970,10 +2954,6 @@ This is your own one-time link!</source>
|
||||
<target>Error al parar SimpleX</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>¡Error al cambiar perfil!</target>
|
||||
@@ -4204,10 +4184,6 @@ This is your link for group %@!</source>
|
||||
<target>Servidores de mensajes</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>El autor del mensaje se mantiene privado.</target>
|
||||
@@ -5753,10 +5729,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
<target>Seleccionar</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Seleccionados %lld</target>
|
||||
@@ -6127,10 +6099,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
<target>Compartir enlace</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Comparte este enlace de un solo uso</target>
|
||||
@@ -6471,10 +6439,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Tomar foto</target>
|
||||
@@ -7755,10 +7719,6 @@ Repeat connection request?</source>
|
||||
<target>Mis perfiles</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>El contacto ha enviado un archivo mayor al máximo admitido (%@).</target>
|
||||
|
||||
@@ -133,10 +133,6 @@
|
||||
<target>%@ haluaa muodostaa yhteyden!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1620,10 +1616,6 @@ This is your own one-time link!</source>
|
||||
<target>Ydinversio: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2552,10 +2544,6 @@ This is your own one-time link!</source>
|
||||
<target>Virhe osoitteenvaihdossa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Virhe roolin vaihdossa</target>
|
||||
@@ -2566,10 +2554,6 @@ This is your own one-time link!</source>
|
||||
<target>Virhe asetuksen muuttamisessa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2778,10 +2762,6 @@ This is your own one-time link!</source>
|
||||
<target>Virhe keskustelun lopettamisessa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Virhe profiilin vaihdossa!</target>
|
||||
@@ -3938,10 +3918,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5375,10 +5351,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Valitse</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5723,10 +5695,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Jaa linkki</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -6042,10 +6010,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Ota kuva</target>
|
||||
@@ -7230,10 +7194,6 @@ Repeat connection request?</source>
|
||||
<target>Keskusteluprofiilisi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ veut se connecter !</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ et %lld membres</target>
|
||||
@@ -1744,10 +1740,6 @@ Il s'agit de votre propre lien unique !</target>
|
||||
<target>Version du cœur : v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Corriger le nom pour %@ ?</target>
|
||||
@@ -2732,10 +2724,6 @@ Il s'agit de votre propre lien unique !</target>
|
||||
<target>Erreur de changement d'adresse</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Erreur lors du changement de rôle</target>
|
||||
@@ -2746,10 +2734,6 @@ Il s'agit de votre propre lien unique !</target>
|
||||
<target>Erreur de changement de paramètre</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard.</target>
|
||||
@@ -2970,10 +2954,6 @@ Il s'agit de votre propre lien unique !</target>
|
||||
<target>Erreur lors de l'arrêt du chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Erreur lors du changement de profil !</target>
|
||||
@@ -4204,10 +4184,6 @@ Voici votre lien pour le groupe %@ !</target>
|
||||
<target>Serveurs de messages</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>La source du message reste privée.</target>
|
||||
@@ -5753,10 +5729,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
|
||||
<target>Choisir</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>%lld sélectionné(s)</target>
|
||||
@@ -6127,10 +6099,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
|
||||
<target>Partager le lien</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Partager ce lien d'invitation unique</target>
|
||||
@@ -6471,10 +6439,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Prendre une photo</target>
|
||||
@@ -7755,10 +7719,6 @@ Répéter la demande de connexion ?</target>
|
||||
<target>Vos profils de chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ kapcsolódni szeretne!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ és további %lld tag</target>
|
||||
@@ -1744,10 +1740,6 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
<target>Alapverziószám: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Név javítása erre: %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
<target>Hiba a cím megváltoztatásakor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Hiba a szerepkör megváltoztatásakor</target>
|
||||
@@ -2746,10 +2734,6 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
<target>Hiba a beállítás megváltoztatásakor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később.</target>
|
||||
@@ -2970,10 +2954,6 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
<target>Hiba a csevegés megállításakor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Hiba a profil váltásakor!</target>
|
||||
@@ -4204,10 +4184,6 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!</target>
|
||||
<target>Üzenetkiszolgálók</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Az üzenet forrása titokban marad.</target>
|
||||
@@ -5753,10 +5729,6 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
|
||||
<target>Választás</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>%lld kiválasztva</target>
|
||||
@@ -6127,10 +6099,6 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
|
||||
<target>Hivatkozás megosztása</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Egyszer használatos meghívó hivatkozás megosztása</target>
|
||||
@@ -6471,10 +6439,6 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Kép készítése</target>
|
||||
@@ -7755,10 +7719,6 @@ Kapcsolódási kérés megismétlése?</target>
|
||||
<target>Csevegési profilok</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ si vuole connettere!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ e %lld membri</target>
|
||||
@@ -1744,10 +1740,6 @@ Questo è il tuo link una tantum!</target>
|
||||
<target>Versione core: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Correggere il nome a %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ Questo è il tuo link una tantum!</target>
|
||||
<target>Errore nella modifica dell'indirizzo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Errore nel cambio di ruolo</target>
|
||||
@@ -2746,10 +2734,6 @@ Questo è il tuo link una tantum!</target>
|
||||
<target>Errore nella modifica dell'impostazione</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Errore di connessione al server di inoltro %@. Riprova più tardi.</target>
|
||||
@@ -2970,10 +2954,6 @@ Questo è il tuo link una tantum!</target>
|
||||
<target>Errore nell'interruzione della chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Errore nel cambio di profilo!</target>
|
||||
@@ -4204,10 +4184,6 @@ Questo è il tuo link per il gruppo %@!</target>
|
||||
<target>Server dei messaggi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>La fonte del messaggio resta privata.</target>
|
||||
@@ -5753,10 +5729,6 @@ Attivalo nelle impostazioni *Rete e server*.</target>
|
||||
<target>Seleziona</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>%lld selezionato</target>
|
||||
@@ -6127,10 +6099,6 @@ Attivalo nelle impostazioni *Rete e server*.</target>
|
||||
<target>Condividi link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Condividi questo link di invito una tantum</target>
|
||||
@@ -6471,10 +6439,6 @@ Attivalo nelle impostazioni *Rete e server*.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Scatta foto</target>
|
||||
@@ -7755,10 +7719,6 @@ Ripetere la richiesta di connessione?</target>
|
||||
<target>I tuoi profili di chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ が接続を希望しています!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@や%@など%lld人のメンバー</target>
|
||||
@@ -1644,10 +1640,6 @@ This is your own one-time link!</source>
|
||||
<target>コアのバージョン: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2577,10 +2569,6 @@ This is your own one-time link!</source>
|
||||
<target>アドレス変更にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>役割変更にエラー発生</target>
|
||||
@@ -2591,10 +2579,6 @@ This is your own one-time link!</source>
|
||||
<target>設定変更にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2803,10 +2787,6 @@ This is your own one-time link!</source>
|
||||
<target>チャット停止にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>プロフィール切り替えにエラー発生!</target>
|
||||
@@ -3962,10 +3942,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5400,10 +5376,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>選択</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5741,10 +5713,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>リンクを送る</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -6061,10 +6029,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>写真を撮影</target>
|
||||
@@ -7248,10 +7212,6 @@ Repeat connection request?</source>
|
||||
<target>あなたのチャットプロフィール</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ wil verbinding maken!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ en %lld leden</target>
|
||||
@@ -1744,10 +1740,6 @@ Dit is uw eigen eenmalige link!</target>
|
||||
<target>Core versie: v% @</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Juiste naam voor %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ Dit is uw eigen eenmalige link!</target>
|
||||
<target>Fout bij wijzigen van adres</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Fout bij wisselen van rol</target>
|
||||
@@ -2746,10 +2734,6 @@ Dit is uw eigen eenmalige link!</target>
|
||||
<target>Fout bij wijzigen van instelling</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw.</target>
|
||||
@@ -2970,10 +2954,6 @@ Dit is uw eigen eenmalige link!</target>
|
||||
<target>Fout bij het stoppen van de chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Fout bij wisselen van profiel!</target>
|
||||
@@ -4204,10 +4184,6 @@ Dit is jouw link voor groep %@!</target>
|
||||
<target>Berichtservers</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Berichtbron blijft privé.</target>
|
||||
@@ -5753,10 +5729,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
|
||||
<target>Selecteer</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>%lld geselecteerd</target>
|
||||
@@ -6127,10 +6099,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
|
||||
<target>Deel link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Deel deze eenmalige uitnodigingslink</target>
|
||||
@@ -6471,10 +6439,6 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Foto nemen</target>
|
||||
@@ -7755,10 +7719,6 @@ Verbindingsverzoek herhalen?</target>
|
||||
<target>Uw chat profielen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ chce się połączyć!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ i %lld członków</target>
|
||||
@@ -1744,10 +1740,6 @@ To jest twój jednorazowy link!</target>
|
||||
<target>Wersja rdzenia: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Poprawić imię na %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ To jest twój jednorazowy link!</target>
|
||||
<target>Błąd zmiany adresu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Błąd zmiany roli</target>
|
||||
@@ -2746,10 +2734,6 @@ To jest twój jednorazowy link!</target>
|
||||
<target>Błąd zmiany ustawienia</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później.</target>
|
||||
@@ -2970,10 +2954,6 @@ To jest twój jednorazowy link!</target>
|
||||
<target>Błąd zatrzymania czatu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Błąd przełączania profilu!</target>
|
||||
@@ -4204,10 +4184,6 @@ To jest twój link do grupy %@!</target>
|
||||
<target>Serwery wiadomości</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Źródło wiadomości pozostaje prywatne.</target>
|
||||
@@ -5753,10 +5729,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
<target>Wybierz</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Zaznaczono %lld</target>
|
||||
@@ -6127,10 +6099,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
<target>Udostępnij link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Udostępnij ten jednorazowy link</target>
|
||||
@@ -6471,10 +6439,6 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Zrób zdjęcie</target>
|
||||
@@ -7755,10 +7719,6 @@ Powtórzyć prośbę połączenia?</target>
|
||||
<target>Twoje profile czatu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@).</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ хочет соединиться!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ и %lld членов группы</target>
|
||||
@@ -1744,10 +1740,6 @@ This is your own one-time link!</source>
|
||||
<target>Версия ядра: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Исправить имя на %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ This is your own one-time link!</source>
|
||||
<target>Ошибка при изменении адреса</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Ошибка при изменении роли</target>
|
||||
@@ -2746,10 +2734,6 @@ This is your own one-time link!</source>
|
||||
<target>Ошибка при изменении настройки</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Ошибка подключения к пересылающему серверу %@. Попробуйте позже.</target>
|
||||
@@ -2970,10 +2954,6 @@ This is your own one-time link!</source>
|
||||
<target>Ошибка при остановке чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Ошибка выбора профиля!</target>
|
||||
@@ -4204,10 +4184,6 @@ This is your link for group %@!</source>
|
||||
<target>Серверы сообщений</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Источник сообщения остаётся конфиденциальным.</target>
|
||||
@@ -5753,10 +5729,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Выбрать</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Выбрано %lld</target>
|
||||
@@ -6127,10 +6099,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Поделиться ссылкой</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Поделиться одноразовой ссылкой-приглашением</target>
|
||||
@@ -6471,10 +6439,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Сделать фото</target>
|
||||
@@ -7755,10 +7719,6 @@ Repeat connection request?</source>
|
||||
<target>Ваши профили чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).</target>
|
||||
|
||||
@@ -129,10 +129,6 @@
|
||||
<target>%@ อยากเชื่อมต่อ!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1610,10 +1606,6 @@ This is your own one-time link!</source>
|
||||
<target>รุ่นหลัก: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2538,10 +2530,6 @@ This is your own one-time link!</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target>
|
||||
@@ -2552,10 +2540,6 @@ This is your own one-time link!</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2763,10 +2747,6 @@ This is your own one-time link!</source>
|
||||
<target>เกิดข้อผิดพลาดในการหยุดแชท</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target>
|
||||
@@ -3921,10 +3901,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5352,10 +5328,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>เลือก</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5698,10 +5670,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>แชร์ลิงก์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -6015,10 +5983,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>ถ่ายภาพ</target>
|
||||
@@ -7199,10 +7163,6 @@ Repeat connection request?</source>
|
||||
<target>โปรไฟล์แชทของคุณ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ bağlanmak istiyor!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ ve %lld üyeleri</target>
|
||||
@@ -1693,10 +1689,6 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
|
||||
<target>Çekirdek sürümü: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>İsim %@ olarak düzeltilsin mi?</target>
|
||||
@@ -2661,10 +2653,6 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
|
||||
<target>Adres değiştirilirken hata oluştu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Rol değiştirilirken hata oluştu</target>
|
||||
@@ -2675,10 +2663,6 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
|
||||
<target>Ayar değiştirilirken hata oluştu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2894,10 +2878,6 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
|
||||
<target>Sohbet durdurulurken hata oluştu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Profil değiştirilirken hata oluştu!</target>
|
||||
@@ -4104,10 +4084,6 @@ Bu senin grup için bağlantın %@!</target>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Mesaj kaynağı gizli kalır.</target>
|
||||
@@ -5609,10 +5585,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Seç</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5966,10 +5938,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Bağlantıyı paylaş</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Bu tek kullanımlık bağlantı davetini paylaş</target>
|
||||
@@ -6296,10 +6264,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_TVLDEKAL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Fotoğraf çek</target>
|
||||
@@ -7553,10 +7517,6 @@ Bağlantı isteği tekrarlansın mı?</target>
|
||||
<target>Sohbet profillerin</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi.</target>
|
||||
|
||||
@@ -137,10 +137,6 @@
|
||||
<target>%@ хоче підключитися!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ та %lld учасників</target>
|
||||
@@ -1744,10 +1740,6 @@ This is your own one-time link!</source>
|
||||
<target>Основна версія: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<target>Виправити ім'я на %@?</target>
|
||||
@@ -2732,10 +2724,6 @@ This is your own one-time link!</source>
|
||||
<target>Помилка зміни адреси</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Помилка зміни ролі</target>
|
||||
@@ -2746,10 +2734,6 @@ This is your own one-time link!</source>
|
||||
<target>Помилка зміни налаштування</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Помилка підключення до сервера переадресації %@. Спробуйте пізніше.</target>
|
||||
@@ -2970,10 +2954,6 @@ This is your own one-time link!</source>
|
||||
<target>Помилка зупинки чату</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>Помилка перемикання профілю!</target>
|
||||
@@ -4204,10 +4184,6 @@ This is your link for group %@!</source>
|
||||
<target>Сервери повідомлень</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>Джерело повідомлення залишається приватним.</target>
|
||||
@@ -5753,10 +5729,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Виберіть</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Вибрано %lld</target>
|
||||
@@ -6127,10 +6099,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>Поділіться посиланням</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>Поділіться цим одноразовим посиланням-запрошенням</target>
|
||||
@@ -6471,10 +6439,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>Сфотографуйте</target>
|
||||
@@ -7755,10 +7719,6 @@ Repeat connection request?</source>
|
||||
<target>Ваші профілі чату</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@).</target>
|
||||
|
||||
@@ -135,10 +135,6 @@
|
||||
<target>%@ 要连接!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@" xml:space="preserve">
|
||||
<source>%1$@, %2$@</source>
|
||||
<note>format for date separator in chat</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target>%@, %@ 和 %lld 成员</target>
|
||||
@@ -1669,10 +1665,6 @@ This is your own one-time link!</source>
|
||||
<target>核心版本: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Corner" xml:space="preserve">
|
||||
<source>Corner</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve">
|
||||
<source>Correct name to %@?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2626,10 +2618,6 @@ This is your own one-time link!</source>
|
||||
<target>更改地址错误</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve">
|
||||
<source>Error changing connection profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>更改角色错误</target>
|
||||
@@ -2640,10 +2628,6 @@ This is your own one-time link!</source>
|
||||
<target>更改设置错误</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve">
|
||||
<source>Error changing to incognito!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2857,10 +2841,6 @@ This is your own one-time link!</source>
|
||||
<target>停止聊天错误</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile" xml:space="preserve">
|
||||
<source>Error switching profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error switching profile!" xml:space="preserve">
|
||||
<source>Error switching profile!</source>
|
||||
<target>切换资料错误!</target>
|
||||
@@ -4053,10 +4033,6 @@ This is your link for group %@!</source>
|
||||
<source>Message servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message shape" xml:space="preserve">
|
||||
<source>Message shape</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
<source>Message source remains private.</source>
|
||||
<target>消息来源保持私密。</target>
|
||||
@@ -5536,10 +5512,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>选择</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Select chat profile" xml:space="preserve">
|
||||
<source>Select chat profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5889,10 +5861,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>分享链接</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share profile" xml:space="preserve">
|
||||
<source>Share profile</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
|
||||
<source>Share this 1-time invite link</source>
|
||||
<target>分享此一次性邀请链接</target>
|
||||
@@ -6217,10 +6185,6 @@ Enable in *Network & servers* settings.</source>
|
||||
<target>TCP_KEEPINTVL</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tail" xml:space="preserve">
|
||||
<source>Tail</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Take picture" xml:space="preserve">
|
||||
<source>Take picture</source>
|
||||
<target>拍照</target>
|
||||
@@ -7453,10 +7417,6 @@ Repeat connection request?</source>
|
||||
<target>您的聊天资料</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
|
||||
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>您的联系人发送的文件大于当前支持的最大大小 (%@)。</target>
|
||||
|
||||
@@ -339,9 +339,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
"displayName": invitation.contact.displayName,
|
||||
"contactId": invitation.contact.id,
|
||||
"callUUID": invitation.callUUID ?? "",
|
||||
"media": invitation.callType.media.rawValue,
|
||||
"callTs": invitation.callTs.timeIntervalSince1970
|
||||
"media": invitation.callType.media.rawValue
|
||||
]) { error in
|
||||
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
||||
|
||||
@@ -104,7 +104,7 @@ class ShareModel: ObservableObject {
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = imageFromBase64(profileImage) {
|
||||
let uiImage = UIImage(base64Encoded: profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@ struct ShareView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func imagePreview(_ imgStr: String) -> some View {
|
||||
if let img = imageFromBase64(imgStr) {
|
||||
@ViewBuilder private func imagePreview(_ img: String) -> some View {
|
||||
if let img = UIImage(base64Encoded: img) {
|
||||
previewArea {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
@@ -163,7 +163,7 @@ struct ShareView: View {
|
||||
@ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View {
|
||||
previewArea {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
@@ -194,7 +194,6 @@
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; };
|
||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
||||
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; };
|
||||
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; };
|
||||
@@ -215,11 +214,11 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E5BD84572C832BF9008C24D1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84522C832BF9008C24D1 /* libgmpxx.a */; };
|
||||
E5BD84582C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84532C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a */; };
|
||||
E5BD84592C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84542C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a */; };
|
||||
E5BD845A2C832BF9008C24D1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84552C832BF9008C24D1 /* libgmp.a */; };
|
||||
E5BD845B2C832BF9008C24D1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84562C832BF9008C24D1 /* libffi.a */; };
|
||||
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57B2C78BF95009F2C7C /* libgmpxx.a */; };
|
||||
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57C2C78BF95009F2C7C /* libgmp.a */; };
|
||||
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */; };
|
||||
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */; };
|
||||
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57F2C78BF95009F2C7C /* libffi.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
@@ -533,7 +532,6 @@
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; };
|
||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
||||
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||
@@ -552,11 +550,11 @@
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E5BD84522C832BF9008C24D1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E5BD84532C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a"; sourceTree = "<group>"; };
|
||||
E5BD84542C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5BD84552C832BF9008C24D1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5BD84562C832BF9008C24D1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E51ED57C2C78BF95009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a"; sourceTree = "<group>"; };
|
||||
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E51ED57F2C78BF95009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -647,14 +645,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E5BD84572C832BF9008C24D1 /* libgmpxx.a in Frameworks */,
|
||||
E5BD845A2C832BF9008C24D1 /* libgmp.a in Frameworks */,
|
||||
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */,
|
||||
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */,
|
||||
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */,
|
||||
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E5BD845B2C832BF9008C24D1 /* libffi.a in Frameworks */,
|
||||
E5BD84582C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a in Frameworks */,
|
||||
E5BD84592C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -731,11 +729,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E5BD84562C832BF9008C24D1 /* libffi.a */,
|
||||
E5BD84552C832BF9008C24D1 /* libgmp.a */,
|
||||
E5BD84522C832BF9008C24D1 /* libgmpxx.a */,
|
||||
E5BD84542C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N-ghc9.6.3.a */,
|
||||
E5BD84532C832BF9008C24D1 /* libHSsimplex-chat-6.1.0.0-1tXK6wuT4H71iMwoWdPa4N.a */,
|
||||
E51ED57F2C78BF95009F2C7C /* libffi.a */,
|
||||
E51ED57C2C78BF95009F2C7C /* libgmp.a */,
|
||||
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */,
|
||||
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */,
|
||||
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -791,7 +789,6 @@
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
|
||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */,
|
||||
CE7548092C622630009579B7 /* SwipeLabel.swift */,
|
||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -1492,7 +1489,6 @@
|
||||
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */,
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */,
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
|
||||
@@ -1883,7 +1879,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1908,7 +1904,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1932,7 +1928,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1957,7 +1953,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1973,11 +1969,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1993,11 +1989,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2018,7 +2014,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2033,7 +2029,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2055,7 +2051,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2070,7 +2066,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2092,7 +2088,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2118,7 +2114,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2143,7 +2139,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2169,7 +2165,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2194,7 +2190,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2209,7 +2205,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2228,7 +2224,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 234;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2243,7 +2239,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -540,7 +540,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?)
|
||||
case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo)
|
||||
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo)
|
||||
case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
|
||||
case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
|
||||
case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
|
||||
@@ -1102,12 +1102,12 @@ public enum GroupLinkPlan: Decodable, Hashable {
|
||||
case known(groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
struct NewUser: Encodable {
|
||||
struct NewUser: Encodable, Hashable {
|
||||
var profile: Profile?
|
||||
var pastTimestamp: Bool
|
||||
}
|
||||
|
||||
public enum ChatPagination {
|
||||
public enum ChatPagination: Hashable {
|
||||
case last(count: Int)
|
||||
case after(chatItemId: Int64, count: Int)
|
||||
case before(chatItemId: Int64, count: Int)
|
||||
@@ -1329,7 +1329,7 @@ public struct ServerAddress: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public struct NetCfg: Codable, Equatable, Hashable {
|
||||
public var socksProxy: String? = nil
|
||||
var socksMode: SocksMode = .always
|
||||
public var hostMode: HostMode = .publicHost
|
||||
@@ -1383,18 +1383,18 @@ public struct NetCfg: Codable, Equatable {
|
||||
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
|
||||
}
|
||||
|
||||
public enum HostMode: String, Codable {
|
||||
public enum HostMode: String, Codable, Hashable {
|
||||
case onionViaSocks
|
||||
case onionHost = "onion"
|
||||
case publicHost = "public"
|
||||
}
|
||||
|
||||
public enum SocksMode: String, Codable {
|
||||
public enum SocksMode: String, Codable, Hashable {
|
||||
case always = "always"
|
||||
case onion = "onion"
|
||||
}
|
||||
|
||||
public enum SMPProxyMode: String, Codable, SelectableItem {
|
||||
public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
|
||||
case always = "always"
|
||||
case unknown = "unknown"
|
||||
case unprotected = "unprotected"
|
||||
@@ -1414,7 +1414,7 @@ public enum SMPProxyMode: String, Codable, SelectableItem {
|
||||
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
|
||||
}
|
||||
|
||||
public enum SMPProxyFallback: String, Codable, SelectableItem {
|
||||
public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
|
||||
case allow = "allow"
|
||||
case allowProtected = "allowProtected"
|
||||
case prohibit = "prohibit"
|
||||
@@ -1432,7 +1432,7 @@ public enum SMPProxyFallback: String, Codable, SelectableItem {
|
||||
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
|
||||
}
|
||||
|
||||
public enum OnionHosts: String, Identifiable {
|
||||
public enum OnionHosts: String, Identifiable, Hashable {
|
||||
case no
|
||||
case prefer
|
||||
case require
|
||||
@@ -1466,7 +1466,7 @@ public enum OnionHosts: String, Identifiable {
|
||||
public static let values: [OnionHosts] = [.no, .prefer, .require]
|
||||
}
|
||||
|
||||
public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
|
||||
case user
|
||||
case entity
|
||||
|
||||
@@ -1482,7 +1482,7 @@ public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
public static let values: [TransportSessionMode] = [.user, .entity]
|
||||
}
|
||||
|
||||
public struct KeepAliveOpts: Codable, Equatable {
|
||||
public struct KeepAliveOpts: Codable, Equatable, Hashable {
|
||||
public var keepIdle: Int // seconds
|
||||
public var keepIntvl: Int // seconds
|
||||
public var keepCnt: Int // times
|
||||
@@ -1490,7 +1490,7 @@ public struct KeepAliveOpts: Codable, Equatable {
|
||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||
}
|
||||
|
||||
public enum NetworkStatus: Decodable, Equatable {
|
||||
public enum NetworkStatus: Decodable, Equatable, Hashable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
@@ -1528,7 +1528,7 @@ public enum NetworkStatus: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConnNetworkStatus: Decodable {
|
||||
public struct ConnNetworkStatus: Decodable, Hashable {
|
||||
public var agentConnId: String
|
||||
public var networkStatus: NetworkStatus
|
||||
}
|
||||
@@ -1553,7 +1553,7 @@ public enum MsgFilter: String, Codable, Hashable {
|
||||
case mentions
|
||||
}
|
||||
|
||||
public struct UserMsgReceiptSettings: Codable {
|
||||
public struct UserMsgReceiptSettings: Codable, Hashable {
|
||||
public var enable: Bool
|
||||
public var clearOverrides: Bool
|
||||
|
||||
@@ -1602,7 +1602,7 @@ public enum SndSwitchStatus: String, Codable, Hashable {
|
||||
case sendingQTEST = "sending_qtest"
|
||||
}
|
||||
|
||||
public enum QueueDirection: String, Decodable {
|
||||
public enum QueueDirection: String, Decodable, Hashable {
|
||||
case rcv
|
||||
case snd
|
||||
}
|
||||
@@ -1657,12 +1657,12 @@ public struct AutoAccept: Codable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SelectableItem: Identifiable, Equatable {
|
||||
public protocol SelectableItem: Hashable, Identifiable {
|
||||
var label: LocalizedStringKey { get }
|
||||
static var values: [Self] { get }
|
||||
}
|
||||
|
||||
public struct DeviceToken: Decodable {
|
||||
public struct DeviceToken: Decodable, Hashable {
|
||||
var pushProvider: PushProvider
|
||||
var token: String
|
||||
|
||||
@@ -1676,12 +1676,12 @@ public struct DeviceToken: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PushEnvironment: String {
|
||||
public enum PushEnvironment: String, Hashable {
|
||||
case development
|
||||
case production
|
||||
}
|
||||
|
||||
public enum PushProvider: String, Decodable {
|
||||
public enum PushProvider: String, Decodable, Hashable {
|
||||
case apns_dev
|
||||
case apns_prod
|
||||
|
||||
@@ -1695,7 +1695,7 @@ public enum PushProvider: String, Decodable {
|
||||
|
||||
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
|
||||
// and .local for periodic background checks
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
|
||||
case off = "OFF"
|
||||
case periodic = "PERIODIC"
|
||||
case instant = "INSTANT"
|
||||
@@ -1713,7 +1713,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
||||
}
|
||||
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
|
||||
case hidden
|
||||
case contact
|
||||
case message
|
||||
@@ -1731,7 +1731,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
|
||||
}
|
||||
|
||||
public struct RemoteCtrlInfo: Decodable {
|
||||
public struct RemoteCtrlInfo: Decodable, Hashable {
|
||||
public var remoteCtrlId: Int64
|
||||
public var ctrlDeviceName: String
|
||||
public var sessionState: RemoteCtrlSessionState?
|
||||
@@ -1741,7 +1741,7 @@ public struct RemoteCtrlInfo: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCtrlSessionState: Decodable {
|
||||
public enum RemoteCtrlSessionState: Decodable, Hashable {
|
||||
case starting
|
||||
case searching
|
||||
case connecting
|
||||
@@ -1756,17 +1756,17 @@ public enum RemoteCtrlStopReason: Decodable {
|
||||
case disconnected
|
||||
}
|
||||
|
||||
public struct CtrlAppInfo: Decodable {
|
||||
public struct CtrlAppInfo: Decodable, Hashable {
|
||||
public var appVersionRange: AppVersionRange
|
||||
public var deviceName: String
|
||||
}
|
||||
|
||||
public struct AppVersionRange: Decodable {
|
||||
public struct AppVersionRange: Decodable, Hashable {
|
||||
public var minVersion: String
|
||||
public var maxVersion: String
|
||||
}
|
||||
|
||||
public struct CoreVersionInfo: Decodable {
|
||||
public struct CoreVersionInfo: Decodable, Hashable {
|
||||
public var version: String
|
||||
public var simplexmqVersion: String
|
||||
public var simplexmqCommit: String
|
||||
@@ -2104,14 +2104,14 @@ public enum RemoteCtrlError: Decodable, Hashable {
|
||||
case protocolError
|
||||
}
|
||||
|
||||
public struct MigrationFileLinkData: Codable {
|
||||
public struct MigrationFileLinkData: Codable, Hashable {
|
||||
let networkConfig: NetworkConfig?
|
||||
|
||||
public init(networkConfig: NetworkConfig) {
|
||||
self.networkConfig = networkConfig
|
||||
}
|
||||
|
||||
public struct NetworkConfig: Codable {
|
||||
public struct NetworkConfig: Codable, Hashable {
|
||||
let socksProxy: String?
|
||||
let hostMode: HostMode?
|
||||
let requiredHostMode: Bool?
|
||||
@@ -2143,7 +2143,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public struct AppSettings: Codable, Equatable, Hashable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAskToApproveRelays: Bool? = nil
|
||||
@@ -2238,7 +2238,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum AppSettingsNotificationMode: String, Codable {
|
||||
public enum AppSettingsNotificationMode: String, Codable, Hashable {
|
||||
case off
|
||||
case periodic
|
||||
case instant
|
||||
@@ -2266,13 +2266,13 @@ public enum AppSettingsNotificationMode: String, Codable {
|
||||
// case message
|
||||
//}
|
||||
|
||||
public enum AppSettingsLockScreenCalls: String, Codable {
|
||||
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
|
||||
case disable
|
||||
case show
|
||||
case accept
|
||||
}
|
||||
|
||||
public struct UserNetworkInfo: Codable, Equatable {
|
||||
public struct UserNetworkInfo: Codable, Equatable, Hashable {
|
||||
public let networkType: UserNetworkType
|
||||
public let online: Bool
|
||||
|
||||
@@ -2282,7 +2282,7 @@ public struct UserNetworkInfo: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum UserNetworkType: String, Codable {
|
||||
public enum UserNetworkType: String, Codable, Hashable {
|
||||
case none
|
||||
case cellular
|
||||
case wifi
|
||||
@@ -2300,7 +2300,7 @@ public enum UserNetworkType: String, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RcvMsgInfo: Codable {
|
||||
public struct RcvMsgInfo: Codable, Hashable {
|
||||
var msgId: Int64
|
||||
var msgDeliveryId: Int64
|
||||
var msgDeliveryStatus: String
|
||||
@@ -2308,16 +2308,7 @@ public struct RcvMsgInfo: Codable {
|
||||
var agentMsgMeta: String
|
||||
}
|
||||
|
||||
public struct ServerQueueInfo: Codable {
|
||||
var server: String
|
||||
var rcvId: String
|
||||
var sndId: String
|
||||
var ntfId: String?
|
||||
var status: String
|
||||
var info: QueueInfo
|
||||
}
|
||||
|
||||
public struct QueueInfo: Codable {
|
||||
public struct QueueInfo: Codable, Hashable {
|
||||
var qiSnd: Bool
|
||||
var qiNtf: Bool
|
||||
var qiSub: QSub?
|
||||
@@ -2325,25 +2316,25 @@ public struct QueueInfo: Codable {
|
||||
var qiMsg: MsgInfo?
|
||||
}
|
||||
|
||||
public struct QSub: Codable {
|
||||
public struct QSub: Codable, Hashable {
|
||||
var qSubThread: QSubThread
|
||||
var qDelivered: String?
|
||||
}
|
||||
|
||||
public enum QSubThread: String, Codable {
|
||||
public enum QSubThread: String, Codable, Hashable {
|
||||
case noSub
|
||||
case subPending
|
||||
case subThread
|
||||
case prohibitSub
|
||||
}
|
||||
|
||||
public struct MsgInfo: Codable {
|
||||
public struct MsgInfo: Codable, Hashable {
|
||||
var msgId: String
|
||||
var msgTs: Date
|
||||
var msgType: MsgType
|
||||
}
|
||||
|
||||
public enum MsgType: String, Codable {
|
||||
public enum MsgType: String, Codable, Hashable {
|
||||
case message
|
||||
case quota
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ public let appSuspendTimeout: Int = 15 // seconds
|
||||
|
||||
public let defaultProfileImageCorner: Double = 22.5
|
||||
|
||||
public let defaultChatItemRoundness: Double = 0.8
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||
let GROUP_DEFAULT_SE_STATE = "seState"
|
||||
|
||||
@@ -42,7 +42,6 @@ public struct RcvCallInvitation: Decodable {
|
||||
public var contact: Contact
|
||||
public var callType: CallType
|
||||
public var sharedKey: String?
|
||||
public var callUUID: String?
|
||||
public var callTs: Date
|
||||
public var callTypeText: LocalizedStringKey {
|
||||
get {
|
||||
@@ -53,8 +52,10 @@ public struct RcvCallInvitation: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public var callkitUUID: UUID? = UUID()
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case user, contact, callType, sharedKey, callUUID, callTs
|
||||
case user, contact, callType, sharedKey, callTs
|
||||
}
|
||||
|
||||
public static let sampleData = RcvCallInvitation(
|
||||
|
||||
@@ -2688,13 +2688,6 @@ public enum CIDirection: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func sameDirection(_ dir: CIDirection) -> Bool {
|
||||
switch (self, dir) {
|
||||
case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId
|
||||
default: sent == dir.sent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIMeta: Decodable, Hashable {
|
||||
@@ -2713,7 +2706,7 @@ public struct CIMeta: Decodable, Hashable {
|
||||
public var deletable: Bool
|
||||
public var editable: Bool
|
||||
|
||||
public var timestampText: Text { Text(formatTimestampMeta(itemTs)) }
|
||||
public var timestampText: Text { get { formatTimestampText(itemTs) } }
|
||||
public var recent: Bool { updatedAt + 10 > .now }
|
||||
public var isLive: Bool { itemLive == true }
|
||||
public var disappearing: Bool { !isRcvNew && itemTimed?.deleteAt != nil }
|
||||
@@ -2723,6 +2716,10 @@ public struct CIMeta: Decodable, Hashable {
|
||||
return false
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? {
|
||||
itemStatus.statusIcon(metaColor, primaryColor)
|
||||
}
|
||||
|
||||
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta {
|
||||
CIMeta(
|
||||
itemId: id,
|
||||
@@ -2768,10 +2765,6 @@ public func formatTimestampText(_ date: Date) -> Text {
|
||||
Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat))
|
||||
}
|
||||
|
||||
public func formatTimestampMeta(_ date: Date) -> String {
|
||||
date.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
private func recent(_ date: Date) -> Bool {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
@@ -2812,22 +2805,21 @@ public enum CIStatus: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
|
||||
public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? {
|
||||
switch self {
|
||||
case .sndNew: nil
|
||||
case let .sndSent(sndProgress):
|
||||
(Image("checkmark.wide"), sndProgress == .partial ? paleMetaColor : metaColor)
|
||||
case let .sndRcvd(msgRcptStatus, sndProgress):
|
||||
case .sndNew: return nil
|
||||
case .sndSent: return ("checkmark", metaColor)
|
||||
case let .sndRcvd(msgRcptStatus, _):
|
||||
switch msgRcptStatus {
|
||||
case .ok: (Image("checkmark.2"), sndProgress == .partial ? paleMetaColor : metaColor)
|
||||
case .badMsgHash: (Image("checkmark.2"), .red)
|
||||
case .ok: return ("checkmark", metaColor)
|
||||
case .badMsgHash: return ("checkmark", .red)
|
||||
}
|
||||
case .sndErrorAuth: (Image(systemName: "multiply"), .red)
|
||||
case .sndError: (Image(systemName: "multiply"), .red)
|
||||
case .sndWarning: (Image(systemName: "exclamationmark.triangle.fill"), .orange)
|
||||
case .rcvNew: (Image(systemName: "circlebadge.fill"), primaryColor)
|
||||
case .rcvRead: nil
|
||||
case .invalid: (Image(systemName: "questionmark"), metaColor)
|
||||
case .sndErrorAuth: return ("multiply", .red)
|
||||
case .sndError: return ("multiply", .red)
|
||||
case .sndWarning: return ("exclamationmark.triangle.fill", .orange)
|
||||
case .rcvNew: return ("circlebadge.fill", primaryColor)
|
||||
case .rcvRead: return nil
|
||||
case .invalid: return ("questionmark", metaColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2922,20 +2914,20 @@ public enum GroupSndStatus: Decodable, Hashable {
|
||||
case warning(agentError: SndError)
|
||||
case invalid(text: String)
|
||||
|
||||
public func statusIcon(_ metaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color) {
|
||||
public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color) {
|
||||
switch self {
|
||||
case .new: (Image(systemName: "ellipsis"), metaColor)
|
||||
case .forwarded: (Image(systemName: "chevron.forward.2"), metaColor)
|
||||
case .inactive: (Image(systemName: "person.badge.minus"), metaColor)
|
||||
case .sent: (Image("checkmark.wide"), metaColor)
|
||||
case .new: return ("ellipsis", metaColor)
|
||||
case .forwarded: return ("chevron.forward.2", metaColor)
|
||||
case .inactive: return ("person.badge.minus", metaColor)
|
||||
case .sent: return ("checkmark", metaColor)
|
||||
case let .rcvd(msgRcptStatus):
|
||||
switch msgRcptStatus {
|
||||
case .ok: (Image("checkmark.2"), metaColor)
|
||||
case .badMsgHash: (Image("checkmark.2"), .red)
|
||||
case .ok: return ("checkmark", metaColor)
|
||||
case .badMsgHash: return ("checkmark", .red)
|
||||
}
|
||||
case .error: (Image(systemName: "multiply"), .red)
|
||||
case .warning: (Image(systemName: "exclamationmark.triangle.fill"), .orange)
|
||||
case .invalid: (Image(systemName: "questionmark"), metaColor)
|
||||
case .error: return ("multiply", .red)
|
||||
case .warning: return ("exclamationmark.triangle.fill", .orange)
|
||||
case .invalid: return ("questionmark", metaColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,30 +383,16 @@ extension UIImage {
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func imageFromBase64(_ base64Encoded: String?) -> UIImage? {
|
||||
if let base64Encoded {
|
||||
if let img = imageCache.object(forKey: base64Encoded as NSString) {
|
||||
return img
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(base64Encoded)),
|
||||
let img = UIImage(data: data) {
|
||||
imageCache.setObject(img, forKey: base64Encoded as NSString)
|
||||
return img
|
||||
public convenience init?(base64Encoded: String?) {
|
||||
if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) {
|
||||
self.init(data: data)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var imageCache: NSCache<NSString, UIImage> = {
|
||||
var cache = NSCache<NSString, UIImage>()
|
||||
cache.countLimit = 1000
|
||||
return cache
|
||||
}()
|
||||
|
||||
public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in
|
||||
|
||||
@@ -15,7 +15,7 @@ android {
|
||||
namespace = "chat.simplex.app"
|
||||
minSdk = 26
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 34
|
||||
targetSdk = 33
|
||||
// !!!
|
||||
// skip version code after release to F-Droid, as it uses two version codes
|
||||
versionCode = (extra["android.version_code"] as String).toInt()
|
||||
@@ -126,29 +126,29 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
|
||||
//implementation("androidx.compose.material:material:$compose_version")
|
||||
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
|
||||
implementation("androidx.activity:activity-compose:1.9.1")
|
||||
val workVersion = "2.9.1"
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
|
||||
implementation("androidx.activity:activity-compose:1.8.2")
|
||||
val workVersion = "2.9.0"
|
||||
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
||||
implementation("androidx.work:work-multiprocess:$workVersion")
|
||||
|
||||
implementation("com.jakewharton:process-phoenix:3.0.0")
|
||||
implementation("com.jakewharton:process-phoenix:2.2.0")
|
||||
|
||||
//Camera Permission
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
|
||||
|
||||
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
|
||||
//implementation("androidx.compose.ui:ui-util:$compose_version")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:1.6.4")
|
||||
}
|
||||
|
||||
@@ -21,12 +21,6 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<!-- Requirements that allows to specify foreground service types -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<application
|
||||
android:name="SimplexApp"
|
||||
android:allowBackup="false"
|
||||
@@ -139,9 +133,7 @@
|
||||
android:name=".SimplexService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
/>
|
||||
android:stopWithTask="false"></service>
|
||||
|
||||
<!-- SimplexService restart on reboot -->
|
||||
|
||||
@@ -149,9 +141,7 @@
|
||||
android:name=".CallService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
|
||||
/>
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".CallService$CallActionReceiver"
|
||||
|
||||
@@ -2,18 +2,17 @@ package chat.simplex.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.*
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.model.NtfManager.EndCallAction
|
||||
import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.common.model.NotificationPreviewMode
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.CallState
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
@@ -35,7 +34,7 @@ class CallService: Service() {
|
||||
} else {
|
||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
||||
}
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -43,7 +42,8 @@ class CallService: Service() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Call service created")
|
||||
notificationManager = createNotificationChannel()
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType())
|
||||
updateNotification()
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -69,14 +69,7 @@ class CallService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationIfNeeded(): Notification {
|
||||
val ntf = serviceNotification
|
||||
if (ntf != null) return ntf
|
||||
|
||||
return updateNotification()
|
||||
}
|
||||
|
||||
fun updateNotification(): Notification {
|
||||
fun updateNotification() {
|
||||
val call = chatModel.activeCall.value
|
||||
val previewMode = appPreferences.notificationPreviewMode.get()
|
||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
@@ -90,31 +83,8 @@ class CallService: Service() {
|
||||
else
|
||||
base64ToBitmap(image).asAndroidBitmap()
|
||||
|
||||
val ntf = createNotification(title, text, largeIcon, call?.connectedAt)
|
||||
serviceNotification = ntf
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType())
|
||||
return ntf
|
||||
}
|
||||
|
||||
private fun foregroundServiceType(): Int {
|
||||
val call = chatModel.activeCall.value
|
||||
return if (call == null) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (call.supportsVideo()) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 29) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
} else {
|
||||
0
|
||||
}
|
||||
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(): NotificationManager? {
|
||||
|
||||
@@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() {
|
||||
SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
processIntent(intent)
|
||||
processExternalIntent(intent)
|
||||
|
||||
@@ -5,8 +5,9 @@ import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.initChatControllerOnStart
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Date
|
||||
@@ -29,12 +30,12 @@ object MessagesFetcherWorker {
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
|
||||
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
Log.d(TAG, "Worker: canceled all tasks")
|
||||
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.*
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -36,7 +37,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider {
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
@@ -119,10 +120,7 @@ class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider
|
||||
* */
|
||||
if (chatModel.chatRunning.value != false &&
|
||||
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
|
||||
// New installation passes all checks above and tries to start the service which is not needed at all
|
||||
// because preferred notification type is not yet chosen. So, check that the user has initialized db already
|
||||
appPrefs.newDatabaseInitialized.get()
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
|
||||
) {
|
||||
SimplexService.start()
|
||||
}
|
||||
@@ -163,7 +161,7 @@ class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider
|
||||
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
.build()
|
||||
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
|
||||
getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
}
|
||||
|
||||
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
|
||||
@@ -366,9 +364,4 @@ class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for an exception:
|
||||
// WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder().build()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.os.SystemClock
|
||||
@@ -16,10 +15,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||
import chat.simplex.common.model.ChatController
|
||||
@@ -55,15 +52,18 @@ class SimplexService: Service() {
|
||||
} else {
|
||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
||||
}
|
||||
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
|
||||
return START_STICKY // to restart if killed
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Simplex service created")
|
||||
createNotificationIfNeeded()
|
||||
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
val title = generalGetString(MR.strings.simplex_service_notification_title)
|
||||
val text = generalGetString(MR.strings.simplex_service_notification_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
serviceNotification = createNotification(title, text)
|
||||
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
|
||||
/**
|
||||
* The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and
|
||||
* we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown.
|
||||
@@ -103,26 +103,6 @@ class SimplexService: Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationIfNeeded(): Notification {
|
||||
val ntf = serviceNotification
|
||||
if (ntf != null) return ntf
|
||||
|
||||
val title = generalGetString(MR.strings.simplex_service_notification_title)
|
||||
val text = generalGetString(MR.strings.simplex_service_notification_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
val newNtf = createNotification(title, text)
|
||||
serviceNotification = newNtf
|
||||
return newNtf
|
||||
}
|
||||
|
||||
private fun foregroundServiceType(): Int {
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
Log.d(TAG, "SimplexService startService")
|
||||
if (wakeLock != null || isCheckingNewMessages) return
|
||||
@@ -292,7 +272,7 @@ class SimplexService: Service() {
|
||||
|
||||
fun scheduleStart(context: Context) {
|
||||
Log.d(TAG, "Enqueuing work to start subscriber service")
|
||||
val workManager = context.getWorkManagerInstance()
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
|
||||
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
|
||||
}
|
||||
@@ -312,10 +292,6 @@ class SimplexService: Service() {
|
||||
}
|
||||
|
||||
private suspend fun serviceAction(action: Action) {
|
||||
if (!NtfManager.areNotificationsEnabledInSystem()) {
|
||||
Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service")
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(androidAppContext, SimplexService::class.java).also {
|
||||
|
||||
+1
-3
@@ -53,7 +53,7 @@ object NtfManager {
|
||||
private val msgNtfTimeoutMs = 30000L
|
||||
|
||||
init {
|
||||
if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert()
|
||||
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
|
||||
}
|
||||
|
||||
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
|
||||
@@ -287,8 +287,6 @@ object NtfManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled()
|
||||
|
||||
/**
|
||||
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
|
||||
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
|
||||
|
||||
+5
-6
@@ -120,7 +120,6 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
return grantedAudio && grantedCamera
|
||||
}
|
||||
|
||||
@Deprecated("Was deprecated in OS")
|
||||
override fun onBackPressed() {
|
||||
if (isOnLockScreenNow()) {
|
||||
super.onBackPressed()
|
||||
@@ -140,7 +139,6 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
enterPictureInPictureMode()
|
||||
@@ -250,9 +248,6 @@ fun CallActivityView() {
|
||||
)
|
||||
if (permissionsState.allPermissionsGranted) {
|
||||
ActiveCallView()
|
||||
LaunchedEffect(Unit) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
} else {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -290,6 +285,11 @@ fun CallActivityView() {
|
||||
AlertManager.shared.showInView()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(call == null) {
|
||||
if (call != null) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
||||
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
||||
Log.d(TAG, "CallActivityView: finishing activity")
|
||||
@@ -424,7 +424,6 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
callUUID = "",
|
||||
remoteHostId = null,
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="highOrLowLight">#8b8786</color>
|
||||
<color name="window_background_dark">#121212</color>
|
||||
</resources>
|
||||
@@ -61,8 +61,8 @@ kotlin {
|
||||
val androidMain by getting {
|
||||
kotlin.srcDir("build/generated/moko/androidMain/src")
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-compose:1.9.1")
|
||||
val workVersion = "2.9.1"
|
||||
implementation("androidx.activity:activity-compose:1.8.2")
|
||||
val workVersion = "2.9.0"
|
||||
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
||||
implementation("com.google.accompanist:accompanist-insets:0.30.1")
|
||||
|
||||
@@ -78,22 +78,22 @@ kotlin {
|
||||
//Camera Permission
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
|
||||
|
||||
implementation("androidx.webkit:webkit:1.11.0")
|
||||
implementation("androidx.webkit:webkit:1.10.0")
|
||||
|
||||
// GIFs support
|
||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||
implementation("io.coil-kt:coil-gif:2.6.0")
|
||||
|
||||
implementation("com.jakewharton:process-phoenix:3.0.0")
|
||||
implementation("com.jakewharton:process-phoenix:2.2.0")
|
||||
|
||||
val cameraXVersion = "1.3.4"
|
||||
val cameraXVersion = "1.3.2"
|
||||
implementation("androidx.camera:camera-core:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-view:${cameraXVersion}")
|
||||
|
||||
// Calls lifecycle listener
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
|
||||
}
|
||||
}
|
||||
val desktopMain by getting {
|
||||
@@ -102,7 +102,7 @@ kotlin {
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
|
||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.12")
|
||||
implementation("uk.co.caprica:vlcj:4.8.3")
|
||||
implementation("uk.co.caprica:vlcj:4.8.2")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
@@ -119,8 +119,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
testOptions.targetSdk = 34
|
||||
lint.targetSdk = 34
|
||||
testOptions.targetSdk = 33
|
||||
lint.targetSdk = 33
|
||||
val isAndroid = gradle.startParameter.taskNames.find {
|
||||
val lower = it.lowercase()
|
||||
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
|
||||
|
||||
-15
@@ -6,8 +6,6 @@ import android.net.LocalServerSocket
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import java.io.*
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
@@ -74,16 +72,3 @@ fun initHaskell() {
|
||||
|
||||
initHS()
|
||||
}
|
||||
|
||||
fun Context.getWorkManagerInstance(): WorkManager {
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672
|
||||
if (!WorkManager.isInitialized()) {
|
||||
try {
|
||||
WorkManager.initialize(this, Configuration.Builder().build())
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
return WorkManager.getInstance(this)
|
||||
}
|
||||
|
||||
-2
@@ -770,7 +770,6 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||
@@ -800,7 +799,6 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||
|
||||
+1
-2
@@ -14,7 +14,6 @@ actual fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
usingLAMode: LAMode,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val activity = mainActivity.get() ?: return completed(LAResult.Error(""))
|
||||
@@ -28,7 +27,7 @@ actual fun authenticate(
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -31,6 +33,7 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.saveAppLocale
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -79,7 +82,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
@@ -120,7 +123,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
val item = AppIcon.values()[index]
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.work.WorkManager
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -32,7 +33,7 @@ fun restartApp() {
|
||||
}
|
||||
|
||||
private fun shutdownApp() {
|
||||
androidAppContext.getWorkManagerInstance().cancelAllWork()
|
||||
WorkManager.getInstance(androidAppContext).cancelAllWork()
|
||||
platform.androidServiceSafeStop()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="#8b8786" />
|
||||
<solid android:color="@color/highOrLowLight" />
|
||||
<size android:width="1dp" />
|
||||
</shape>
|
||||
|
||||
@@ -74,7 +74,6 @@ fun MainScreen() {
|
||||
LaunchedEffect(showAdvertiseLAAlert) {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.laNoticeShown.get()
|
||||
&& !appPrefs.performLA.get()
|
||||
&& showAdvertiseLAAlert
|
||||
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.size > 3
|
||||
@@ -212,8 +211,10 @@ fun MainScreen() {
|
||||
} else {
|
||||
ActiveCallView()
|
||||
}
|
||||
} else {
|
||||
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
}
|
||||
ModalManager.fullscreen.showOneTimePasscodeInView()
|
||||
AlertManager.privacySensitive.showInView()
|
||||
if (onboarding == OnboardingStage.OnboardingComplete) {
|
||||
LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
@@ -32,7 +31,7 @@ object AppLock {
|
||||
|
||||
fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get() && !appPrefs.performLA.get()) {
|
||||
if (!laNoticeShown.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
|
||||
@@ -58,8 +57,6 @@ object AppLock {
|
||||
|
||||
private fun showChooseLAMode() {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.la_lock_mode),
|
||||
text = null,
|
||||
@@ -83,23 +80,21 @@ object AppLock {
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock),
|
||||
generalGetString(MR.strings.auth_confirm_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.showAuthScreen.value = true
|
||||
m.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
@@ -109,22 +104,19 @@ object AppLock {
|
||||
}
|
||||
|
||||
private fun setPasscode() {
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
val appPrefs = ChatController.appPrefs
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
ChatModel.showAuthScreen.value = true
|
||||
ChatModel.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
appPrefs.laMode.set(LAMode.PASSCODE)
|
||||
laTurnedOnAlert()
|
||||
},
|
||||
cancel = {
|
||||
ChatModel.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
ChatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close = close
|
||||
@@ -155,7 +147,6 @@ object AppLock {
|
||||
else
|
||||
generalGetString(MR.strings.auth_unlock),
|
||||
selfDestruct = true,
|
||||
oneTime = false,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
@@ -169,7 +160,7 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
@@ -201,23 +192,22 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.showAuthScreen.value = true
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
@@ -237,13 +227,12 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(MR.strings.auth_disable_simplex_lock),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
val selfDestructPref = m.controller.appPrefs.selfDestruct
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
DatabaseUtils.ksAppPassword.remove()
|
||||
selfDestructPref.set(false)
|
||||
@@ -251,12 +240,12 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.showAuthScreen.value = true
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.showAuthScreen.value = false
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
|
||||
+4
-13
@@ -20,6 +20,7 @@ import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.internal.ChannelFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.*
|
||||
@@ -97,7 +98,7 @@ object ChatModel {
|
||||
}
|
||||
)
|
||||
}
|
||||
val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
|
||||
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
|
||||
|
||||
@@ -411,9 +412,7 @@ object ChatModel {
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.removeAll {
|
||||
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
|
||||
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
|
||||
val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt
|
||||
val remove = it.id == cItem.id
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
@@ -785,8 +784,7 @@ object ChatModel {
|
||||
data class ShowingInvitation(
|
||||
val connId: String,
|
||||
val connReq: String,
|
||||
val connChatUsed: Boolean,
|
||||
val conn: PendingContactConnection
|
||||
val connChatUsed: Boolean
|
||||
)
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -987,7 +985,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val fullName get() = contact.fullName
|
||||
override val image get() = contact.image
|
||||
override val localAlias: String get() = contact.localAlias
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase)
|
||||
|
||||
companion object {
|
||||
val sampleData = Direct(Contact.sampleData)
|
||||
@@ -1222,12 +1219,6 @@ data class Contact(
|
||||
override val localAlias get() = profile.localAlias
|
||||
val verified get() = activeConn?.connectionCode != null
|
||||
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean {
|
||||
val s = searchAnyCase.trim().lowercase()
|
||||
return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s)
|
||||
}
|
||||
|
||||
|
||||
val directOrUsed: Boolean get() =
|
||||
if (activeConn != null) {
|
||||
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
|
||||
|
||||
+6
-46
@@ -1030,14 +1030,14 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
|
||||
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
|
||||
val r = sendCmd(rh, CC.APIContactQueueInfo(contactId))
|
||||
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
|
||||
apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
|
||||
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
|
||||
val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId))
|
||||
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
|
||||
apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r)
|
||||
@@ -1132,30 +1132,9 @@ object ChatController {
|
||||
|
||||
suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito))
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionIncognitoUpdated -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId))
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionUserChanged -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
if (r is CR.ConnectionIncognitoUpdated) return r.toConnection
|
||||
Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? {
|
||||
@@ -2937,7 +2916,6 @@ sealed class CC {
|
||||
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
|
||||
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
|
||||
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
|
||||
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
@@ -3093,7 +3071,6 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
|
||||
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
|
||||
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
|
||||
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
@@ -3236,7 +3213,6 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "apiVerifyGroupMember"
|
||||
is APIAddContact -> "apiAddContact"
|
||||
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
|
||||
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
|
||||
is APIConnectPlan -> "apiConnectPlan"
|
||||
is APIConnect -> "apiConnect"
|
||||
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
|
||||
@@ -4763,7 +4739,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR()
|
||||
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR()
|
||||
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: ServerQueueInfo): CR()
|
||||
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR()
|
||||
@Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@@ -4781,7 +4757,6 @@ sealed class CR {
|
||||
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
|
||||
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
|
||||
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@@ -4960,7 +4935,6 @@ sealed class CR {
|
||||
is ConnectionVerified -> "connectionVerified"
|
||||
is Invitation -> "invitation"
|
||||
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
||||
is ConnectionUserChanged -> "ConnectionUserChanged"
|
||||
is CRConnectionPlan -> "connectionPlan"
|
||||
is SentConfirmation -> "sentConfirmation"
|
||||
is SentInvitation -> "sentInvitation"
|
||||
@@ -5129,7 +5103,6 @@ sealed class CR {
|
||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
||||
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
|
||||
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
||||
is SentConfirmation -> withUser(user, json.encodeToString(connection))
|
||||
is SentInvitation -> withUser(user, json.encodeToString(connection))
|
||||
@@ -5348,7 +5321,6 @@ abstract class TerminalItem {
|
||||
val date: Instant = Clock.System.now()
|
||||
abstract val label: String
|
||||
abstract val details: String
|
||||
val createdAtNanos: Long = System.nanoTime()
|
||||
|
||||
class Cmd(override val id: Long, override val remoteHostId: Long?, val cmd: CC): TerminalItem() {
|
||||
override val label get() = "> ${cmd.cmdString}"
|
||||
@@ -5581,7 +5553,6 @@ sealed class ChatErrorType {
|
||||
is AgentCommandError -> "agentCommandError"
|
||||
is InvalidFileDescription -> "invalidFileDescription"
|
||||
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
|
||||
is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited"
|
||||
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
|
||||
is InternalError -> "internalError"
|
||||
is CEException -> "exception $message"
|
||||
@@ -5659,7 +5630,6 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
|
||||
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
|
||||
@@ -6444,16 +6414,6 @@ data class RcvMsgInfo (
|
||||
val agentMsgMeta: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerQueueInfo (
|
||||
val server: String,
|
||||
val rcvId: String,
|
||||
val sndId: String,
|
||||
val ntfId: String? = null,
|
||||
val status: String,
|
||||
val info: QueueInfo
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QueueInfo (
|
||||
val qiSnd: Boolean,
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ fun TerminalLog() {
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumnWithScrollBar(reverseLayout = true) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
items(reversedTerminalItems) { item ->
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
Text(
|
||||
|
||||
-1
@@ -47,7 +47,6 @@ class CallManager(val chatModel: ChatModel) {
|
||||
remoteHostId = invitation.remoteHostId,
|
||||
userProfile = userProfile,
|
||||
contact = invitation.contact,
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
|
||||
-1
@@ -115,7 +115,6 @@ fun PreviewIncomingCallAlertLayout() {
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
callUUID = "",
|
||||
callTs = Clock.System.now()
|
||||
),
|
||||
chatModel = ChatModel,
|
||||
|
||||
-2
@@ -13,7 +13,6 @@ data class Call(
|
||||
val remoteHostId: Long?,
|
||||
val userProfile: Profile,
|
||||
val contact: Contact,
|
||||
val callUUID: String?,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
@@ -106,7 +105,6 @@ sealed class WCallResponse {
|
||||
val contact: Contact,
|
||||
val callType: CallType,
|
||||
val sharedKey: String? = null,
|
||||
val callUUID: String,
|
||||
val callTs: Instant
|
||||
) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
|
||||
+1
-1
@@ -1268,7 +1268,7 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
|
||||
fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
||||
val (rcvMsgInfo, qInfo) = info
|
||||
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
|
||||
return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo)
|
||||
|
||||
+5
-5
@@ -282,14 +282,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val versions = ciInfo.itemVersions
|
||||
if (versions.isNotEmpty()) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
versions.forEachIndexed { i, ciVersion ->
|
||||
ItemVersionView(ciVersion, current = i == 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
QuotedMsgView(qi)
|
||||
}
|
||||
@@ -381,14 +381,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status, sentViaProxy) ->
|
||||
MemberDeliveryStatusView(member, status, sentViaProxy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
+2
-3
@@ -86,7 +86,6 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
@@ -545,7 +544,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
@@ -998,7 +997,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
)
|
||||
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem ->
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
|
||||
+8
-7
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
@@ -249,8 +249,9 @@ fun AddGroupMembersButton(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ModalData.GroupChatInfoLayout(
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
currentUser: User,
|
||||
@@ -271,12 +272,12 @@ fun ModalData.GroupChatInfoLayout(
|
||||
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val listState = remember { appBarHandler.listState }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
KeyChangeEffect(chat.id) {
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
val filteredMembers = remember(members) {
|
||||
derivedStateOf {
|
||||
val s = searchText.value.text.trim().lowercase()
|
||||
@@ -508,11 +509,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp),
|
||||
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
MemberProfileImage(size = 42.dp, member)
|
||||
MemberProfileImage(size = DEFAULT_MIN_SECTION_ITEM_HEIGHT, member)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -673,7 +674,7 @@ private fun SearchRowView(
|
||||
@Composable
|
||||
fun PreviewGroupChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
ModalData().GroupChatInfoLayout(
|
||||
GroupChatInfoLayout(
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ fun ErrorChatListItem() {
|
||||
|
||||
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -80,7 +80,7 @@ private fun ShareListNavLinkLayout(
|
||||
click: () -> Unit,
|
||||
stopped: Boolean,
|
||||
) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 8.dp), click = click, disabled = stopped) {
|
||||
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
|
||||
chatLinkPreview()
|
||||
}
|
||||
Divider(Modifier.padding(horizontal = 8.dp))
|
||||
@@ -98,11 +98,9 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (chat.chatInfo is ChatInfo.Local) {
|
||||
ProfileImage(size = 42.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
|
||||
ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
|
||||
} else {
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image)
|
||||
ProfileImage(size = 46.dp, chat.chatInfo.image)
|
||||
}
|
||||
Text(
|
||||
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+4
-11
@@ -94,17 +94,10 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(
|
||||
chatModel,
|
||||
userPickerState,
|
||||
showSettings = false,
|
||||
showCancel = true,
|
||||
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
|
||||
cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
)
|
||||
UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -64,7 +66,7 @@ fun DatabaseErrorView(
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content = content)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
-1
@@ -423,7 +423,6 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null,
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_stop_chat),
|
||||
generalGetString(MR.strings.auth_log_in_using_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user