Compare commits

..

19 Commits
12.rc ... main

Author SHA1 Message Date
Kévin Commaille
e3c34328ee
utils: Avoid to use a temp file for decoding images when possible
By using the new API from glycin.
2025-08-13 11:45:22 +02:00
Kévin Commaille
0e9d34dd9d
Upgrade glycin
Tests the beta for GNOME 49 with the loaders in the Flatpak runtime.
2025-08-13 11:45:22 +02:00
Kévin Commaille
4597519128
utils: Simplify TokioDrop API
It is now just a wrapper.
2025-08-13 11:45:22 +02:00
Kévin Commaille
8a7c690d21
session: Do not expose Matrix client as property 2025-08-13 11:45:22 +02:00
Kévin Commaille
07cc8f9787
Upgrade matrix-sdk
Brings in fixes for sending media with the unauthenticated endpoints.
2025-08-13 10:28:28 +02:00
Kévin Commaille
2a09a76fb0
Upgrade crate dependencies
Just run `cargo update`.
2025-08-11 15:22:48 +02:00
Kévin Commaille
ddc5001a79
Release Fractal 12 2025-08-11 13:35:54 +02:00
Kévin Commaille
ae53630df3
Fix new clippy lints 2025-08-11 13:35:54 +02:00
Alexandre Franke
6f090f3883 Update French translation 2025-08-11 10:53:01 +00:00
Artur S0
b343862f35 Update Russian translation 2025-08-08 12:00:32 +00:00
Anders Jonsson
c7c9d5d974 Update Swedish translation 2025-08-06 18:29:06 +00:00
Kévin Commaille
10ed8358f9
Upgrade ruma and matrix-sdk
Brings in important bug fixes.
2025-08-06 18:40:09 +02:00
Rafael Fontenelle
0be7615056 Update Brazilian Portuguese translation 2025-08-06 01:49:36 +00:00
Ekaterine Papava
a0955e225a Update Georgian translation 2025-08-05 23:20:44 +00:00
Luming Zh
2ae6f75938 Update Chinese (China) translation 2025-08-02 00:10:48 +00:00
Davide Ferracin
420ec4d24e Update Italian translation 2025-08-01 15:53:08 +00:00
Evan Paterakis
7bd9e7fa45 notification-count: center label to background 2025-08-01 11:36:36 +00:00
Martin
e9f6873d8a Update Slovenian translation 2025-08-01 10:38:41 +00:00
Yuri Chornoivan
9dcee3a6ac Update Ukrainian translation 2025-07-31 18:28:41 +00:00
27 changed files with 11024 additions and 4331 deletions

472
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "fractal"
version = "12.0.0-rc"
version = "12.0.0"
authors = ["Julian Sparber <julian@sparber.net>"]
edition = "2024"
rust-version = "1.85"
@ -61,7 +61,7 @@ zeroize = "1"
# gtk-rs project and dependents. These usually need to be updated together.
adw = { package = "libadwaita", version = "0.7", features = ["v1_7"] }
glycin = { version = "2", default-features = false, features = ["tokio", "gdk4"] }
glycin = { version = "3.0.0-beta.1", default-features = false, features = ["tokio", "gdk4"] }
gst = { version = "0.23", package = "gstreamer" }
gst_app = { version = "0.23", package = "gstreamer-app" }
gst_pbutils = { version = "0.23", package = "gstreamer-pbutils" }
@ -74,23 +74,23 @@ sourceview = { package = "sourceview5", version = "0.9" }
[dependencies.matrix-sdk]
# version = "0.13"
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
rev = "ada68e11144507afc9d178f4264452aae1ff9e27"
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
features = ["socks", "sso-login", "markdown", "qrcode"]
[dependencies.matrix-sdk-store-encryption]
# version = "0.13"
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
rev = "ada68e11144507afc9d178f4264452aae1ff9e27"
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
[dependencies.matrix-sdk-ui]
# version = "0.13"
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
rev = "ada68e11144507afc9d178f4264452aae1ff9e27"
rev = "a9ce1c6e5822b8eb8411c5bc257049d9a9d15884"
[dependencies.ruma]
# version = "0.12.5"
git = "https://github.com/ruma/ruma.git"
rev = "de19ebaf71af620eb17abaefd92e43153f9d041d"
rev = "a2fe858133ba932b4bda730dc7472c9c985739a0"
features = [
"client-api-c",
"markdown",

View File

@ -38,7 +38,7 @@ development version while keeping the stable release around for daily use.
### Stable version
The current stable version is 11.2 (released June 10th 2025).
The current stable version is 12 (released August 11th 2025).
You can get the official Fractal Flatpak from Flathub.
@ -53,7 +53,7 @@ You can get the official Fractal Flatpak from Flathub.
### Beta version
The current beta version is 12.rc (released July 31st 2025).
The current beta version is 12 (same as stable).
It is available as a Flatpak on Flathub Beta.

View File

@ -95,27 +95,6 @@
}
]
},
{
"name": "glycin-loaders",
"buildsystem": "meson",
"config-opts": [
"-Dtests=false",
"-Dlibglycin=false",
"-Dintrospection=false",
"-Dvapi=false",
"-Dcapi_docs=false",
"-Dpython_tests=false"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/sophie-h/glycin.git",
"tag": "1.2.2",
"commit": "c7d362287303944721cf583d4d9e9f7721bfa407",
"disable-submodules": true
}
]
},
{
"name": "fractal",
"buildsystem": "meson",

View File

@ -35,15 +35,15 @@
<screenshots>
<screenshot type="default">
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/main.png</image>
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-12/screenshots/main.png</image>
<caption>Fractals main window</caption>
</screenshot>
<screenshot>
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/media-history.png</image>
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-12/screenshots/media-history.png</image>
<caption>View the media history of a Matrix room</caption>
</screenshot>
<screenshot>
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-11/screenshots/adaptive.png</image>
<image type="source">https://gitlab.gnome.org/World/fractal/raw/fractal-12/screenshots/adaptive.png</image>
<caption>Fractals interface adapts to small screens</caption>
</screenshot>
</screenshots>
@ -71,66 +71,44 @@
</content_rating>
<releases>@development-release@
<release version="12~rc" type="development" date="2025-07-31">
<release version="12" type="stable" date="2025-08-11">
<description>
<p>
Want to get a head start and try out Fractal 12 before its release? Thats what this
Release Candidate is for! New since 12.beta:
Knock, knock, knock… on rooms, baby 🎵 Ooh ooh ooh ooh ooh ooh 🎶 That's right, Fractal 12
adds support for knocking, among other things. Read all about the improvements since 11.2:
</p>
<ul>
<li>
The upcoming room version 12 is supported, with the special power level of room creators
Requesting invites to rooms (aka knocking) is now possible, as is enabling such requests
for room admins.
</li>
<li>
Requesting invites to rooms (aka knocking) is now possible
</li>
<li>
Clicking on the name of the sender of a message adds a mention to them in the composer
</li>
</ul>
<p>
As usual, this release includes other improvements, fixes and new translations thanks to
all our contributors, and our upstream projects.
</p>
<p>
As the version implies, it should be mostly stable and we expect to only include minor
improvements until the release of Fractal 12.
</p>
</description>
</release>
<release version="12~beta" type="development" date="2025-06-26">
<description>
<p>
Hot! Hot! Hot! No, we are not talking about the summer weather in the northern hemisphere,
but about the brand new release of Fractal 12.beta! Coming soon to your device:
</p>
<ul>
<li>
The safety setting to hide media previews in rooms is now synced between Matrix
clients.
</li>
<li>
We added another safety setting (which is also synced) to hide avatars in invites.
The upcoming room version 12 is supported, with the special power level of room
creators.
</li>
<li>
A room can be marked as unread via the context menu in the sidebar.
</li>
<li>
We changed the UX a little for tombstoned rooms. Instead of showing a banner at the top
of the history, it now replaces the composer at the bottom of the history.
</li>
<li>
You can now see if a section in the sidebar has any notifications or activity when it is
collapsed.
</li>
<li>
Clicking on the name of the sender of a message adds a mention to them in the composer.
</li>
<li>
The safety setting to hide media previews in rooms is now synced between Matrix clients
and we added another safety setting (which is also synced) to hide avatars in invites.
</li>
</ul>
<p>
As usual, this release includes other improvements, fixes and new translations thanks to
all our contributors, and our upstream projects.
</p>
<p>
As the version implies, there might be a slight risk of regressions, but it should be
mostly stable. If all goes well the next step is the release candidate!
We want to address special thanks to the translators who worked on this version. We know
this is a huge undertaking and have a deep appreciation for what youve done. If you want
to help with this effort, head over to Damned Lies.
</p>
</description>
</release>

View File

@ -120,8 +120,10 @@ sidebar {
font-weight: bold;
font-size: 0.8em;
border-radius: 9999px;
min-width: 0.7em;
padding: 2px 5px;
min-width: 0.8em;
min-height: 0.8em;
line-height: 0.8em;
padding: 0.4em 5px;
color: currentColor;
background-color: color-mix(in srgb, currentColor 15%, transparent);
}

View File

@ -1,6 +1,6 @@
project('fractal',
'rust',
version: '12.rc',
version: '12',
license: 'GPL-3.0-or-later',
meson_version: '>= 1.1')
@ -11,7 +11,7 @@ base_id = 'org.gnome.Fractal'
application_id = base_id
major_version = '12'
pre_release_version = 'rc'
pre_release_version = ''
version = major_version
if pre_release_version != ''

1776
po/fr.po

File diff suppressed because it is too large Load Diff

7519
po/it.po

File diff suppressed because it is too large Load Diff

393
po/ka.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1784
po/ru.po

File diff suppressed because it is too large Load Diff

123
po/sl.po
View File

@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: fractal master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/fractal/issues\n"
"POT-Creation-Date: 2025-07-29 01:25+0000\n"
"PO-Revision-Date: 2025-07-29 10:53+0200\n"
"POT-Creation-Date: 2025-07-31 20:08+0000\n"
"PO-Revision-Date: 2025-08-01 12:38+0200\n"
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
"Language: sl_SI\n"
@ -594,8 +594,8 @@ msgstr "Zapusti"
#: src/identity_verification_view/confirm_qr_code_page.ui:52
#: src/session/view/account_settings/general_page/mod.rs:391
#: src/session/view/content/room_details/edit_details_subpage.rs:267
#: src/session/view/content/room_details/general_page.rs:1033
#: src/session/view/content/room_details/room_upgrade_dialog.rs:116
#: src/session/view/content/room_details/general_page.rs:1036
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:112
#: src/session/view/content/room_history/event_actions/group.rs:583
#: src/session/view/content/room_history/event_actions/group.rs:647
#: src/session/view/content/room_history/message_toolbar/mod.ui:76
@ -1869,7 +1869,7 @@ msgstr "Člani {room}"
msgid "Any registered user"
msgstr "Vsak registriran uporabnik"
#: src/session/model/room/join_rule.rs:288 src/session/view/content/room_details/general_page.rs:939
#: src/session/model/room/join_rule.rs:288 src/session/view/content/room_details/general_page.rs:942
msgid "Unsupported rule"
msgstr "Nepodprto pravilo"
@ -2903,7 +2903,7 @@ msgid "Remove “{address}”"
msgstr "Odstrani »{address}«"
#: src/session/view/content/room_details/addresses_subpage/mod.rs:431
#: src/session/view/content/room_details/general_page.rs:677 src/session/view/create_room_dialog.ui:132
#: src/session/view/content/room_details/general_page.rs:680 src/session/view/create_room_dialog.ui:132
msgid "Main Address"
msgstr "Glavni naslov"
@ -3048,8 +3048,8 @@ msgstr "Opis"
msgid "Save Description"
msgstr "Shrani opis"
#: src/session/view/content/room_details/general_page.rs:462
#: src/session/view/content/room_details/general_page.rs:508
#: src/session/view/content/room_details/general_page.rs:465
#: src/session/view/content/room_details/general_page.rs:511
#: src/session/view/content/room_details/invite_subpage/list.rs:260
msgid "Member"
msgid_plural "Members"
@ -3058,71 +3058,71 @@ msgstr[1] "člani"
msgstr[2] "člani"
msgstr[3] "člani"
#: src/session/view/content/room_details/general_page.rs:632
#: src/session/view/content/room_details/general_page.rs:635
msgid "Could not change notifications setting"
msgstr "Nastavitve obvestil ni bilo mogoče spremeniti"
#: src/session/view/content/room_details/general_page.rs:672
#: src/session/view/content/room_details/general_page.rs:711
#: src/session/view/content/room_details/general_page.rs:675
#: src/session/view/content/room_details/general_page.rs:714
msgid "Copy address"
msgstr "Kopiraj naslov"
#: src/session/view/content/room_details/general_page.rs:673
#: src/session/view/content/room_details/general_page.rs:712
#: src/session/view/content/room_details/general_page.rs:676
#: src/session/view/content/room_details/general_page.rs:715
msgid "Address copied to clipboard"
msgstr "Naslov je kopiran v odložišče"
#: src/session/view/content/room_details/general_page.rs:754
#: src/session/view/content/room_details/general_page.rs:757
msgid "Room link copied to clipboard"
msgstr "Povezava do klepetalnice kopirana v odložišče"
#: src/session/view/content/room_details/general_page.rs:815
#: src/session/view/content/room_details/general_page.rs:818
msgid "Could not change guest access"
msgstr "Ni bilo mogoče spremeniti dostopa za goste"
#. Translators: Do NOT translate the content between '{' and '}',
#. this is a variable name.
#: src/session/view/content/room_details/general_page.rs:832
#: src/session/view/content/room_details/general_page.rs:835
msgid "Publish in the {homeserver} directory"
msgstr "Objavi v imeniku {homeserver}"
#: src/session/view/content/room_details/general_page.rs:907
#: src/session/view/content/room_details/general_page.rs:910
msgid "Could not publish room in directory"
msgstr "Klepetalnice ni bilo mogoče objaviti v imeniku"
#: src/session/view/content/room_details/general_page.rs:909
#: src/session/view/content/room_details/general_page.rs:912
msgid "Could not unpublish room from directory"
msgstr "Objave klepetalnice v imeniku ni bilo mogoče preklicati"
#: src/session/view/content/room_details/general_page.rs:930
#: src/session/view/content/room_details/general_page.rs:933
#: src/session/view/content/room_details/general_page.ui:274
msgid "Anyone, even if they are not in the room"
msgstr "Kdorkoli, tudi če ni v klepetalnici"
#: src/session/view/content/room_details/general_page.rs:933
#: src/session/view/content/room_details/general_page.rs:936
#: src/session/view/content/room_details/general_page.ui:275
msgid "Members only, since this option was selected"
msgstr "Samo člani, saj je bila ta možnost izbrana"
#: src/session/view/content/room_details/general_page.rs:935
#: src/session/view/content/room_details/general_page.rs:938
#: src/session/view/content/room_details/general_page.ui:277
msgid "Members only, since they were invited"
msgstr "Samo člani, saj so bili povabljeni"
#: src/session/view/content/room_details/general_page.rs:937
#: src/session/view/content/room_details/general_page.rs:940
#: src/session/view/content/room_details/general_page.ui:276
msgid "Members only, since they joined the room"
msgstr "Samo člani, ker so se pridružili klepetalnici"
#: src/session/view/content/room_details/general_page.rs:986
#: src/session/view/content/room_details/general_page.rs:989
msgid "Could not change who can read history"
msgstr "Ni bilo mogoče spremeniti, kdo sme brati zgodovino"
#: src/session/view/content/room_details/general_page.rs:1028
#: src/session/view/content/room_details/general_page.rs:1031
msgid "Enable Encryption?"
msgstr "Želite omogočiti šifriranja?"
#: src/session/view/content/room_details/general_page.rs:1029
#: src/session/view/content/room_details/general_page.rs:1032
msgid ""
"Enabling encryption will prevent new members to read the history before they arrived. This cannot be "
"disabled later."
@ -3130,30 +3130,30 @@ msgstr ""
"Če omogočite šifriranje, novi člani ne bodo mogli prebrati zgodovine pred svojo včlanitvijo. Tega "
"pozneje ni možno onemogočiti."
#: src/session/view/content/room_details/general_page.rs:1034 src/session/view/sidebar/mod.rs:320
#: src/session/view/content/room_details/general_page.rs:1037 src/session/view/sidebar/mod.rs:320
#: src/session/view/sidebar/mod.rs:326
msgid "Enable"
msgstr "Omogoči"
#: src/session/view/content/room_details/general_page.rs:1045
#: src/session/view/content/room_details/general_page.rs:1048
msgid "Could not enable encryption"
msgstr "Šifriranja ni bilo mogoče omogočiti"
#. Translators: As in, 'Room federated'.
#: src/session/view/content/room_details/general_page.rs:1071
#: src/session/view/content/room_details/general_page.rs:1107
msgid "Federated"
msgstr "Združeno"
#. Translators: As in, 'Room not federated'.
#: src/session/view/content/room_details/general_page.rs:1074
#: src/session/view/content/room_details/general_page.rs:1110
msgid "Not federated"
msgstr "Ni združeno"
#: src/session/view/content/room_details/general_page.rs:1105
#: src/session/view/content/room_details/general_page.rs:1144
msgid "Room upgraded successfully"
msgstr "Klepetalnica uspešno nadgrajena"
#: src/session/view/content/room_details/general_page.rs:1109
#: src/session/view/content/room_details/general_page.rs:1148
msgid "Could not upgrade room"
msgstr "Klepetalnice ni bilo mogoče nadgraditi"
@ -3570,7 +3570,7 @@ msgstr "Preklopi iskanje članov klepetalnice"
msgid "Search for room members"
msgstr "Poišči člane klepetalnice"
#: src/session/view/content/room_details/mod.rs:149
#: src/session/view/content/room_details/mod.rs:150
msgid "The user is not in the room members list anymore"
msgstr "Uporabnika ni več na seznamu članov klepetalnice"
@ -3680,7 +3680,8 @@ msgid "Change Server Access Control List"
msgstr "Spremeni seznam za nadzor dostopa do strežnika"
#: src/session/view/content/room_details/permissions/permissions_subpage.ui:191
#: src/session/view/content/room_details/room_upgrade_dialog.rs:111
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:4
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:43
msgid "Upgrade Room"
msgstr "Nadgradi klepetalnico"
@ -3710,20 +3711,51 @@ msgid "Default Power Level"
msgstr "Privzeta raven moči"
#. Translators: As in 'Stable version'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:85
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:114
msgid "Stable"
msgstr "Stabilno"
#. Translators: As in 'Experimental version'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:87
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:117
msgid "Experimental"
msgstr "Poskusno"
#: src/session/view/content/room_details/room_upgrade_dialog.rs:94
msgid "Version"
msgstr "Različica"
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:196
msgid ""
"After the upgrade, you will be the only creator in the room. The other creator will be demoted to the "
"default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The other creators will be demoted to the "
"default power level."
msgstr[0] ""
"Po nadgradnji boste edini avtor v klepetalnici. Drugi avtorji bodo degradirani na privzeto raven moči."
msgstr[1] ""
"Po nadgradnji boste edini avtor v klepetalnici. Drug avtor bo degradiran na privzeto raven moči."
msgstr[2] ""
"Po nadgradnji boste edini avtor v klepetalnici. Druga avtorja bosta degradirana na privzeto raven moči."
msgstr[3] ""
"Po nadgradnji boste edini avtor v klepetalnici. Drugi avtorji bodo degradirani na privzeto raven moči."
#: src/session/view/content/room_details/room_upgrade_dialog.rs:112
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:202
msgid ""
"After the upgrade, you will be the only creator in the room. The current creator will be demoted to the "
"default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The current creators will be demoted to "
"the default power level."
msgstr[0] ""
"Po nadgradnji boste edini avtor v klepetalnici. Trenutni avtorji bodo degradirani na privzeto raven "
"moči."
msgstr[1] ""
"Po nadgradnji boste edini avtor v klepetalnici. Trenutni avtor bo degradiran na privzeto raven moči."
msgstr[2] ""
"Po nadgradnji boste edini avtor v klepetalnici. Trenutna avtorja bosta degradirana na privzeto raven "
"moči."
msgstr[3] ""
"Po nadgradnji boste edini avtor v klepetalnici. Trenutni avtorji bodo degradirani na privzeto raven "
"moči."
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:59
msgid ""
"Upgrading a room to a more recent version allows to benefit from new features from the Matrix "
"specification. It can also be used to reset the room state, which should make the room faster to join. "
@ -3735,8 +3767,12 @@ msgstr ""
"klepetalnico hitreje pridružili. Vendar pa je to treba uporabljati zmerno, ker je lahko moteče, saj se "
"morajo člani klepetalnice ročno pridružiti novi klepetalnici."
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:74
msgid "Version"
msgstr "Različica"
#. Translators: In this string, 'Upgrade' is a verb, as in 'Upgrade Room'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:118
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:120
msgid "Upgrade"
msgstr "Nadgradi"
@ -3964,7 +4000,7 @@ msgstr "Poslano"
msgid "Edited"
msgstr "Urejeno"
#: src/session/view/content/room_history/message_row/mod.rs:245
#: src/session/view/content/room_history/message_row/mod.rs:243
msgid "Sent at {time}"
msgstr "Poslano {time}"
@ -3990,6 +4026,11 @@ msgstr "Odzivi"
msgid "In Reply To"
msgstr "Odgovor na"
#. Translators: This is a verb, as in 'Mention user'.
#: src/session/view/content/room_history/message_row/sender_name.rs:188
msgid "Mention"
msgstr "Omeni"
#. Translators: this is the fallback title for an expander.
#: src/session/view/content/room_history/message_row/text/widgets.rs:439
msgid "Details"

1140
po/sv.po

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: fractal master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/fractal/issues\n"
"POT-Creation-Date: 2025-07-30 15:22+0000\n"
"PO-Revision-Date: 2025-07-30 21:56+0300\n"
"POT-Creation-Date: 2025-07-31 17:47+0000\n"
"PO-Revision-Date: 2025-07-31 21:28+0300\n"
"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
"Language: uk\n"
@ -687,7 +687,7 @@ msgstr "Полишити"
#: src/session/view/account_settings/general_page/mod.rs:391
#: src/session/view/content/room_details/edit_details_subpage.rs:267
#: src/session/view/content/room_details/general_page.rs:1036
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:98
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:112
#: src/session/view/content/room_history/event_actions/group.rs:583
#: src/session/view/content/room_history/event_actions/group.rs:647
#: src/session/view/content/room_history/message_toolbar/mod.ui:76
@ -1115,7 +1115,6 @@ msgid "Confirm Custom Role"
msgstr "Підтвердження нетипової ролі"
#: src/components/power_level_selection/row.ui:101
#| msgid "Advanced Information"
msgid "More Information"
msgstr "Докладніше"
@ -1128,8 +1127,8 @@ msgid ""
"with another one with a different creator, by upgrading the room for example."
msgstr ""
"Автор є незмінною роллю, яку можна пов'язати із декількома користувачами під "
" час створення кімнати. Автори завжди мають вищий рівень прав доступу за"
" будь-якого з учасників кімнати, окрім інших авторів, і мають право роботи у"
"час створення кімнати. Автори завжди мають вищий рівень прав доступу за будь-"
"якого з учасників кімнати, окрім інших авторів, і мають право роботи у "
"кімнаті будь-що. Єдиним способом позбавити автора прав є заміна поточної "
"кімнати на іншу із іншим автором, наприклад, шляхом оновлення кімнати."
@ -3441,20 +3440,20 @@ msgid "Could not enable encryption"
msgstr "Не вдалося увімкнути шифрування"
#. Translators: As in, 'Room federated'.
#: src/session/view/content/room_details/general_page.rs:1095
#: src/session/view/content/room_details/general_page.rs:1107
msgid "Federated"
msgstr "Інтегрований"
#. Translators: As in, 'Room not federated'.
#: src/session/view/content/room_details/general_page.rs:1098
#: src/session/view/content/room_details/general_page.rs:1110
msgid "Not federated"
msgstr "Не федеровано"
#: src/session/view/content/room_details/general_page.rs:1132
#: src/session/view/content/room_details/general_page.rs:1144
msgid "Room upgraded successfully"
msgstr "Кімнату успішно оновлено"
#: src/session/view/content/room_details/general_page.rs:1136
#: src/session/view/content/room_details/general_page.rs:1148
msgid "Could not upgrade room"
msgstr "Не вдалося оновити кімнату"
@ -4019,15 +4018,55 @@ msgid "Default Power Level"
msgstr "Типовий рівень повноважень"
#. Translators: As in 'Stable version'.
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:112
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:114
msgid "Stable"
msgstr "Стабільна"
#. Translators: As in 'Experimental version'.
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:115
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:117
msgid "Experimental"
msgstr "Експериментальна"
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:196
msgid ""
"After the upgrade, you will be the only creator in the room. The other "
"creator will be demoted to the default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The other "
"creators will be demoted to the default power level."
msgstr[0] ""
"Після оновлення ви станете єдиним автором у кімнаті. Інших авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[1] ""
"Після оновлення ви станете єдиним автором у кімнаті. Інших авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[2] ""
"Після оновлення ви станете єдиним автором у кімнаті. Інших авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[3] ""
"Після оновлення ви станете єдиним автором у кімнаті. Іншого автора буде"
" звужено у правах до типового рівня доступу."
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:202
msgid ""
"After the upgrade, you will be the only creator in the room. The current "
"creator will be demoted to the default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The current "
"creators will be demoted to the default power level."
msgstr[0] ""
"Після оновлення ви станете єдиним автором у кімнаті. Поточних авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[1] ""
"Після оновлення ви станете єдиним автором у кімнаті. Поточних авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[2] ""
"Після оновлення ви станете єдиним автором у кімнаті. Поточних авторів буде"
" звужено у правах до типового рівня доступу."
msgstr[3] ""
"Після оновлення ви станете єдиним автором у кімнаті. Поточного автора буде"
" звужено у правах до типового рівня доступу."
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:59
msgid ""
"Upgrading a room to a more recent version allows to benefit from new "
@ -4048,7 +4087,7 @@ msgid "Version"
msgstr "Версія"
#. Translators: In this string, 'Upgrade' is a verb, as in 'Upgrade Room'.
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:106
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:120
msgid "Upgrade"
msgstr "Оновити"
@ -4283,7 +4322,7 @@ msgstr "Надіслано"
msgid "Edited"
msgstr "Змінено"
#: src/session/view/content/room_history/message_row/mod.rs:245
#: src/session/view/content/room_history/message_row/mod.rs:243
msgid "Sent at {time}"
msgstr "Надіслано {time}"
@ -4309,6 +4348,12 @@ msgstr "Реакції"
msgid "In Reply To"
msgstr "У відповідь на"
#. Translators: This is a verb, as in 'Mention user'.
#: src/session/view/content/room_history/message_row/sender_name.rs:188
#| msgid "_Mention"
msgid "Mention"
msgstr "Згадка"
#. Translators: this is the fallback title for an expander.
#: src/session/view/content/room_history/message_row/text/widgets.rs:439
msgid "Details"

View File

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: fractal master\n"
"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/fractal/issues\n"
"POT-Creation-Date: 2025-07-28 17:58+0000\n"
"PO-Revision-Date: 2025-07-29 09:23+0800\n"
"POT-Creation-Date: 2025-08-01 15:53+0000\n"
"PO-Revision-Date: 2025-08-02 08:09+0800\n"
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
"Language: zh_CN\n"
@ -637,8 +637,8 @@ msgstr "离开"
#: src/identity_verification_view/confirm_qr_code_page.ui:52
#: src/session/view/account_settings/general_page/mod.rs:391
#: src/session/view/content/room_details/edit_details_subpage.rs:267
#: src/session/view/content/room_details/general_page.rs:1033
#: src/session/view/content/room_details/room_upgrade_dialog.rs:116
#: src/session/view/content/room_details/general_page.rs:1036
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:112
#: src/session/view/content/room_history/event_actions/group.rs:583
#: src/session/view/content/room_history/event_actions/group.rs:647
#: src/session/view/content/room_history/message_toolbar/mod.ui:76
@ -1938,7 +1938,7 @@ msgid "Any registered user"
msgstr "任何注册的用户"
#: src/session/model/room/join_rule.rs:288
#: src/session/view/content/room_details/general_page.rs:939
#: src/session/view/content/room_details/general_page.rs:942
msgid "Unsupported rule"
msgstr "不支持的规则"
@ -2978,7 +2978,7 @@ msgid "Remove “{address}”"
msgstr "移除“{address}”"
#: src/session/view/content/room_details/addresses_subpage/mod.rs:431
#: src/session/view/content/room_details/general_page.rs:677
#: src/session/view/content/room_details/general_page.rs:680
#: src/session/view/create_room_dialog.ui:132
msgid "Main Address"
msgstr "主地址"
@ -3124,107 +3124,107 @@ msgstr "说明"
msgid "Save Description"
msgstr "保存说明"
#: src/session/view/content/room_details/general_page.rs:462
#: src/session/view/content/room_details/general_page.rs:508
#: src/session/view/content/room_details/general_page.rs:465
#: src/session/view/content/room_details/general_page.rs:511
#: src/session/view/content/room_details/invite_subpage/list.rs:260
msgid "Member"
msgid_plural "Members"
msgstr[0] "成员"
#: src/session/view/content/room_details/general_page.rs:632
#: src/session/view/content/room_details/general_page.rs:635
msgid "Could not change notifications setting"
msgstr "无法更改通知设置"
#: src/session/view/content/room_details/general_page.rs:672
#: src/session/view/content/room_details/general_page.rs:711
#: src/session/view/content/room_details/general_page.rs:675
#: src/session/view/content/room_details/general_page.rs:714
msgid "Copy address"
msgstr "复制地址"
#: src/session/view/content/room_details/general_page.rs:673
#: src/session/view/content/room_details/general_page.rs:712
#: src/session/view/content/room_details/general_page.rs:676
#: src/session/view/content/room_details/general_page.rs:715
msgid "Address copied to clipboard"
msgstr "地址已复制到剪贴板"
#: src/session/view/content/room_details/general_page.rs:754
#: src/session/view/content/room_details/general_page.rs:757
msgid "Room link copied to clipboard"
msgstr "聊天室链接已复制到剪贴板"
#: src/session/view/content/room_details/general_page.rs:815
#: src/session/view/content/room_details/general_page.rs:818
msgid "Could not change guest access"
msgstr "无法更改访客访问权限"
#. Translators: Do NOT translate the content between '{' and '}',
#. this is a variable name.
#: src/session/view/content/room_details/general_page.rs:832
#: src/session/view/content/room_details/general_page.rs:835
msgid "Publish in the {homeserver} directory"
msgstr "公布在 {homeserver} 目录"
#: src/session/view/content/room_details/general_page.rs:907
#: src/session/view/content/room_details/general_page.rs:910
msgid "Could not publish room in directory"
msgstr "无法在目录公布聊天室"
#: src/session/view/content/room_details/general_page.rs:909
#: src/session/view/content/room_details/general_page.rs:912
msgid "Could not unpublish room from directory"
msgstr "无法从目录撤下聊天室"
#: src/session/view/content/room_details/general_page.rs:930
#: src/session/view/content/room_details/general_page.rs:933
#: src/session/view/content/room_details/general_page.ui:274
msgid "Anyone, even if they are not in the room"
msgstr "任何人,即使他们未加入聊天室"
#: src/session/view/content/room_details/general_page.rs:933
#: src/session/view/content/room_details/general_page.rs:936
#: src/session/view/content/room_details/general_page.ui:275
msgid "Members only, since this option was selected"
msgstr "仅成员,在选择该选项之后"
#: src/session/view/content/room_details/general_page.rs:935
#: src/session/view/content/room_details/general_page.rs:938
#: src/session/view/content/room_details/general_page.ui:277
msgid "Members only, since they were invited"
msgstr "仅成员,在他们被邀请之后"
#: src/session/view/content/room_details/general_page.rs:937
#: src/session/view/content/room_details/general_page.rs:940
#: src/session/view/content/room_details/general_page.ui:276
msgid "Members only, since they joined the room"
msgstr "仅成员,在他们加入聊天室后"
#: src/session/view/content/room_details/general_page.rs:986
#: src/session/view/content/room_details/general_page.rs:989
msgid "Could not change who can read history"
msgstr "无法更改历史查阅权限"
#: src/session/view/content/room_details/general_page.rs:1028
#: src/session/view/content/room_details/general_page.rs:1031
msgid "Enable Encryption?"
msgstr "启用加密吗?"
#: src/session/view/content/room_details/general_page.rs:1029
#: src/session/view/content/room_details/general_page.rs:1032
msgid ""
"Enabling encryption will prevent new members to read the history before they "
"arrived. This cannot be disabled later."
msgstr "启用加密将阻止新成员在到达聊天室前查阅聊天历史。该选项无法在随后禁用。"
#: src/session/view/content/room_details/general_page.rs:1034
#: src/session/view/content/room_details/general_page.rs:1037
#: src/session/view/sidebar/mod.rs:320 src/session/view/sidebar/mod.rs:326
msgid "Enable"
msgstr "启用"
#: src/session/view/content/room_details/general_page.rs:1045
#: src/session/view/content/room_details/general_page.rs:1048
msgid "Could not enable encryption"
msgstr "无法启用加密"
#. Translators: As in, 'Room federated'.
#: src/session/view/content/room_details/general_page.rs:1071
#: src/session/view/content/room_details/general_page.rs:1107
msgid "Federated"
msgstr "联合的"
#. Translators: As in, 'Room not federated'.
#: src/session/view/content/room_details/general_page.rs:1074
#: src/session/view/content/room_details/general_page.rs:1110
msgid "Not federated"
msgstr "未联合"
#: src/session/view/content/room_details/general_page.rs:1105
#: src/session/view/content/room_details/general_page.rs:1144
msgid "Room upgraded successfully"
msgstr "聊天室已成功升级"
#: src/session/view/content/room_details/general_page.rs:1109
#: src/session/view/content/room_details/general_page.rs:1148
msgid "Could not upgrade room"
msgstr "无法升级聊天室"
@ -3621,7 +3621,7 @@ msgstr "切换聊天室成员搜索"
msgid "Search for room members"
msgstr "搜索聊天室成员"
#: src/session/view/content/room_details/mod.rs:149
#: src/session/view/content/room_details/mod.rs:150
msgid "The user is not in the room members list anymore"
msgstr "该用户不再在聊天室成员列表中"
@ -3732,7 +3732,8 @@ msgid "Change Server Access Control List"
msgstr "更改服务器访问控制列表"
#: src/session/view/content/room_details/permissions/permissions_subpage.ui:191
#: src/session/view/content/room_details/room_upgrade_dialog.rs:111
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:4
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:43
msgid "Upgrade Room"
msgstr "升级聊天室"
@ -3762,20 +3763,38 @@ msgid "Default Power Level"
msgstr "默认权力等级"
#. Translators: As in 'Stable version'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:85
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:114
msgid "Stable"
msgstr "稳定版"
#. Translators: As in 'Experimental version'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:87
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:117
msgid "Experimental"
msgstr "体验版"
#: src/session/view/content/room_details/room_upgrade_dialog.rs:94
msgid "Version"
msgstr "版本"
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:196
msgid ""
"After the upgrade, you will be the only creator in the room. The other "
"creator will be demoted to the default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The other "
"creators will be demoted to the default power level."
msgstr[0] ""
"升级之后,您将成为该聊天室中唯一的创建者。其他创建者将被降级为默认的权力级"
"别。"
#: src/session/view/content/room_details/room_upgrade_dialog.rs:112
#: src/session/view/content/room_details/upgrade_dialog/mod.rs:202
msgid ""
"After the upgrade, you will be the only creator in the room. The current "
"creator will be demoted to the default power level."
msgid_plural ""
"After the upgrade, you will be the only creator in the room. The current "
"creators will be demoted to the default power level."
msgstr[0] ""
"升级之后,您将成为该聊天室中唯一的创建者。当前的创建者将被降级为默认的权力级"
"别。"
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:59
msgid ""
"Upgrading a room to a more recent version allows to benefit from new "
"features from the Matrix specification. It can also be used to reset the "
@ -3787,8 +3806,12 @@ msgstr ""
"态,可使加入聊天室速度更快。然而由于其可能引起混乱应保守使用,因为聊天室成员"
"需要手动加入新聊天室。"
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:74
msgid "Version"
msgstr "版本"
#. Translators: In this string, 'Upgrade' is a verb, as in 'Upgrade Room'.
#: src/session/view/content/room_details/room_upgrade_dialog.rs:118
#: src/session/view/content/room_details/upgrade_dialog/mod.ui:120
msgid "Upgrade"
msgstr "升级"
@ -4018,7 +4041,7 @@ msgstr "已发送"
msgid "Edited"
msgstr "已编辑"
#: src/session/view/content/room_history/message_row/mod.rs:245
#: src/session/view/content/room_history/message_row/mod.rs:243
msgid "Sent at {time}"
msgstr "在 {time} 发送"
@ -4041,6 +4064,11 @@ msgstr "回应"
msgid "In Reply To"
msgstr "在回复中"
#. Translators: This is a verb, as in 'Mention user'.
#: src/session/view/content/room_history/message_row/sender_name.rs:188
msgid "Mention"
msgstr "提到"
#. Translators: this is the fallback title for an expander.
#: src/session/view/content/room_history/message_row/text/widgets.rs:439
msgid "Details"

View File

@ -6,7 +6,7 @@ use tracing::error;
use crate::{
spawn, spawn_tokio,
utils::{CountedRef, File},
utils::{CountedRef, File, TokioDrop},
};
mod imp {
@ -19,9 +19,12 @@ mod imp {
#[derive(Default)]
pub struct AnimatedImagePaintable {
/// The image loader.
image_loader: OnceCell<Arc<Image<'static>>>,
/// The image decoder.
decoder: OnceCell<Arc<TokioDrop<Image>>>,
/// The file of the image.
///
/// We need to keep a strong reference to the temporary file or it will
/// be destroyed.
file: OnceCell<File>,
/// The current frame that is displayed.
pub(super) current_frame: RefCell<Option<Arc<Frame>>>,
@ -49,7 +52,7 @@ mod imp {
self.current_frame
.borrow()
.as_ref()
.map_or_else(|| self.image_loader().info().height, |f| f.height())
.map_or_else(|| self.decoder().details().height(), |f| f.height())
.try_into()
.unwrap_or(i32::MAX)
}
@ -58,7 +61,7 @@ mod imp {
self.current_frame
.borrow()
.as_ref()
.map_or_else(|| self.image_loader().info().width, |f| f.width())
.map_or_else(|| self.decoder().details().width(), |f| f.width())
.try_into()
.unwrap_or(i32::MAX)
}
@ -94,26 +97,27 @@ mod imp {
}
impl AnimatedImagePaintable {
/// The image loader.
fn image_loader(&self) -> &Arc<Image<'static>> {
self.image_loader
.get()
.expect("image loader is initialized")
/// The image decoder.
fn decoder(&self) -> &Arc<TokioDrop<Image>> {
self.decoder.get().expect("decoder should be initialized")
}
/// Initialize the image.
pub(super) fn init(
&self,
file: File,
image_loader: Arc<Image<'static>>,
decoder: Arc<TokioDrop<Image>>,
first_frame: Arc<Frame>,
file: Option<File>,
) {
self.file.set(file).expect("file is uninitialized");
self.image_loader
.set(image_loader)
.expect("image loader is uninitialized");
self.decoder
.set(decoder)
.expect("decoder should be uninitialized");
self.current_frame.replace(Some(first_frame));
if let Some(file) = file {
self.file.set(file).expect("file should be uninitialized");
}
self.update_animation();
}
@ -198,9 +202,9 @@ mod imp {
}
async fn load_next_frame_inner(&self) {
let image = self.image_loader().clone();
let decoder = self.decoder().clone();
let result = spawn_tokio!(async move { image.next_frame().await })
let result = spawn_tokio!(async move { decoder.next_frame().await })
.await
.unwrap();
@ -229,16 +233,16 @@ glib::wrapper! {
}
impl AnimatedImagePaintable {
/// Construct an `AnimatedImagePaintable` with the given loader and first
/// frame.
/// Construct an `AnimatedImagePaintable` with the given decoder, first
/// frame, and the file containing the image, if any.
pub(crate) fn new(
file: File,
image_loader: Arc<Image<'static>>,
decoder: Arc<TokioDrop<Image>>,
first_frame: Arc<Frame>,
file: Option<File>,
) -> Self {
let obj = glib::Object::new::<Self>();
obj.imp().init(file, image_loader, first_frame);
obj.imp().init(decoder, first_frame, file);
obj
}

View File

@ -316,7 +316,7 @@ mod imp {
if let Some(send_state) = item.send_state() {
match send_state {
EventSendState::NotSentYet => return MessageState::Sending,
EventSendState::NotSentYet { .. } => return MessageState::Sending,
EventSendState::SendingFailed {
error,
is_recoverable,

View File

@ -58,10 +58,6 @@ pub enum SessionState {
Ready = 2,
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedClient")]
pub struct BoxedClient(Client);
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
@ -70,9 +66,8 @@ mod imp {
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Session)]
pub struct Session {
/// The Matrix client.
#[property(construct_only)]
client: TokioDrop<BoxedClient>,
/// The Matrix client for this session.
client: OnceCell<TokioDrop<Client>>,
/// The list model of the sidebar.
#[property(get = Self::sidebar_list_model)]
sidebar_list_model: OnceCell<SidebarListModel>,
@ -128,27 +123,6 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for Session {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.ignored_users.set_session(Some(obj.clone()));
self.notifications.set_session(Some(obj.clone()));
self.user_sessions.init(&obj, obj.user_id().clone());
let monitor = gio::NetworkMonitor::default();
let handler_id = monitor.connect_network_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _| {
spawn!(async move {
imp.update_homeserver_reachable().await;
});
}
));
self.network_monitor_handler_id.replace(Some(handler_id));
}
fn dispose(&self) {
// Needs to be disconnected or else it may restart the sync
if let Some(handler_id) = self.network_monitor_handler_id.take() {
@ -176,9 +150,34 @@ mod imp {
}
impl Session {
// The Matrix client.
/// Set the Matrix client for this session.
pub(super) fn set_client(&self, client: Client) {
self.client
.set(TokioDrop::new(client))
.expect("client should be uninitialized");
let obj = self.obj();
self.ignored_users.set_session(Some(obj.clone()));
self.notifications.set_session(Some(obj.clone()));
self.user_sessions.init(&obj, obj.user_id().clone());
let monitor = gio::NetworkMonitor::default();
let handler_id = monitor.connect_network_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _| {
spawn!(async move {
imp.update_homeserver_reachable().await;
});
}
));
self.network_monitor_handler_id.replace(Some(handler_id));
}
/// The Matrix client for this session.
pub(super) fn client(&self) -> &Client {
&self.client.get().expect("session should be restored").0
self.client.get().expect("client should be initialized")
}
/// The list model of the sidebar.
@ -462,6 +461,7 @@ mod imp {
let sync_settings = SyncSettings::new()
.timeout(Duration::from_secs(30))
.ignore_timeout_on_first_sync(true)
.filter(filter.into());
let mut sync_stream = Box::pin(client.sync_stream(sync_settings).await);
@ -751,11 +751,13 @@ impl Session {
.await
.expect("task was not aborted")?;
Ok(glib::Object::builder()
let obj = glib::Object::builder::<Self>()
.property("info", stored_session)
.property("settings", settings)
.property("client", BoxedClient(client))
.build())
.build();
obj.imp().set_client(client);
Ok(obj)
}
/// Create a new session from the session of the given Matrix client.

View File

@ -953,7 +953,7 @@ mod imp {
row.set_read_only(!is_supported || !can_change);
}
/// Set the history_visibility of the room.
/// Set the history visibility of the room.
#[template_callback]
async fn set_history_visibility(&self) {
let Some(room) = self.room.obj() else {

View File

@ -733,7 +733,7 @@ mod imp {
self.update_changed();
}
/// Handle when the redact_own row has changed.
/// Handle when the `redact_own` row has changed.
#[template_callback]
fn redact_own_changed(&self) {
if self.update_in_progress.get() {
@ -757,7 +757,7 @@ mod imp {
self.update_changed();
}
/// Handle when the redact_others row has changed.
/// Handle when the `redact_others` row has changed.
#[template_callback]
fn redact_others_changed(&self) {
if self.update_in_progress.get() {

View File

@ -813,8 +813,7 @@ mod imp {
}
future::Either::Right((response, _)) => {
// The linux location stream requires a tokio executor when dropped.
let stream_drop = TokioDrop::new();
let _ = stream_drop.set(location_stream);
let _ = TokioDrop::new(location_stream);
if response == gtk::ResponseType::Ok {
break;

View File

@ -290,7 +290,10 @@ mod imp {
})
.collect::<Vec<_>>()
};
groups.into_iter().for_each(|group| group.process_batch());
for group in groups {
group.process_batch();
}
}
/// Handle when items were removed in the underlying model.

View File

@ -127,7 +127,7 @@ impl AnySyncOrStrippedTimelineEvent {
let ev = match raw {
RawAnySyncOrStrippedTimelineEvent::Sync(ev) => Self::Sync(ev.deserialize()?.into()),
RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => {
Self::Stripped(ev.deserialize()?.into())
Self::Stripped(Box::new(ev.deserialize_as()?))
}
};

View File

@ -28,7 +28,10 @@ pub(crate) use queue::{IMAGE_QUEUE, ImageRequestPriority};
use super::{FrameDimensions, MediaFileError};
use crate::{
DISABLE_GLYCIN_SANDBOX, RUNTIME, components::AnimatedImagePaintable, spawn_tokio, utils::File,
DISABLE_GLYCIN_SANDBOX, RUNTIME,
components::AnimatedImagePaintable,
spawn_tokio,
utils::{File, TokioDrop, save_data_to_tmp_file},
};
/// The maximum dimensions of a thumbnail in the timeline.
@ -64,35 +67,66 @@ const THUMBNAIL_DIMENSIONS_THRESHOLD: u32 = 200;
/// [supported image formats of glycin]: https://gitlab.gnome.org/GNOME/glycin/-/tree/main?ref_type=heads#supported-image-formats
const SUPPORTED_ANIMATED_IMAGE_MIME_TYPES: &[&str] = &["image/gif", "image/png", "image/webp"];
/// Get an image loader for the given file.
async fn image_loader(file: gio::File) -> Result<glycin::Image<'static>, glycin::ErrorCtx> {
let mut loader = glycin::Loader::new(file);
/// The source for decoding an image.
enum ImageDecoderSource {
/// The bytes containing the encoded image.
Data(Vec<u8>),
/// The file containing the encoded image.
File(File),
}
impl ImageDecoderSource {
/// The maximum size of the `Data` variant. This is 1 MB.
const MAX_DATA_SIZE: usize = 1_048_576;
/// Construct an `ImageSource` from the given bytes.
///
/// If the size of the bytes are too big to be kept in memory, they are
/// written to a temporary file.
async fn with_bytes(bytes: Vec<u8>) -> Result<Self, MediaFileError> {
if bytes.len() > Self::MAX_DATA_SIZE {
Ok(Self::File(save_data_to_tmp_file(bytes).await?))
} else {
Ok(Self::Data(bytes))
}
}
/// Convert this image source into a loader.
///
/// Returns the created loader, and the image file, if any.
fn into_loader(self) -> (glycin::Loader, Option<File>) {
let (mut loader, file) = match self {
Self::Data(bytes) => (glycin::Loader::new_vec(bytes), None),
Self::File(file) => (glycin::Loader::new(file.as_gfile()), Some(file)),
};
if DISABLE_GLYCIN_SANDBOX {
loader.sandbox_selector(glycin::SandboxSelector::NotSandboxed);
}
spawn_tokio!(async move { loader.load().await })
.await
.unwrap()
(loader, file)
}
/// Load the given file as an image into a `GdkPaintable`.
/// Decode this image source into an [`Image`].
///
/// Set `request_dimensions` if the image will be shown at specific dimensions.
/// To show the image at its natural size, set it to `None`.
async fn load_image(
file: File,
/// Set `request_dimensions` if the image will be shown at specific
/// dimensions. To show the image at its natural size, set it to `None`.
async fn decode_image(
self,
request_dimensions: Option<FrameDimensions>,
) -> Result<Image, glycin::ErrorCtx> {
let image_loader = image_loader(file.as_gfile()).await?;
) -> Result<Image, ImageError> {
let (loader, file) = self.into_loader();
let decoder = spawn_tokio!(async move { loader.load().await })
.await
.expect("task was not aborted")?;
let frame_request = request_dimensions.map(|request| {
let image_info = image_loader.info();
let image_details = decoder.details();
let original_dimensions = FrameDimensions {
width: image_info.width,
height: image_info.height,
width: image_details.width(),
height: image_details.height(),
};
original_dimensions.to_image_loader_request(request)
@ -100,27 +134,44 @@ async fn load_image(
spawn_tokio!(async move {
let first_frame = if let Some(frame_request) = frame_request {
image_loader.specific_frame(frame_request).await?
decoder.specific_frame(frame_request).await?
} else {
image_loader.next_frame().await?
decoder.next_frame().await?
};
Ok(Image {
file,
loader: image_loader.into(),
decoder: TokioDrop::new(decoder).into(),
first_frame: first_frame.into(),
})
})
.await
.expect("task was not aborted")
}
}
impl From<File> for ImageDecoderSource {
fn from(value: File) -> Self {
Self::File(value)
}
}
impl From<gio::File> for ImageDecoderSource {
fn from(value: gio::File) -> Self {
Self::File(value.into())
}
}
/// An image that was just loaded.
#[derive(Clone)]
pub(crate) struct Image {
/// The file of the image.
file: File,
/// The image loader.
loader: Arc<glycin::Image<'static>>,
/// The file containing the image, if any.
///
/// We need to keep a strong reference to the temporary file or it will be
/// destroyed.
file: Option<File>,
/// The image decoder.
decoder: Arc<TokioDrop<glycin::Image>>,
/// The first frame of the image.
first_frame: Arc<glycin::Frame>,
}
@ -134,7 +185,7 @@ impl fmt::Debug for Image {
impl From<Image> for gdk::Paintable {
fn from(value: Image) -> Self {
if value.first_frame.delay().is_some() {
AnimatedImagePaintable::new(value.file, value.loader, value.first_frame).upcast()
AnimatedImagePaintable::new(value.decoder, value.first_frame, value.file).upcast()
} else {
value.first_frame.texture().upcast()
}
@ -157,9 +208,14 @@ impl ImageInfoLoader {
async fn into_first_frame(self) -> Option<Frame> {
match self {
Self::File(file) => {
let image_loader = image_loader(file).await.ok()?;
let handle = spawn_tokio!(async move { image_loader.next_frame().await });
Some(Frame::Glycin(handle.await.unwrap().ok()?))
let (loader, _) = ImageDecoderSource::from(file).into_loader();
let frame = spawn_tokio!(async move { loader.load().await?.next_frame().await })
.await
.expect("task was not aborted")
.ok()?;
Some(Frame::Glycin(frame))
}
Self::Texture(texture) => Some(Frame::Texture(texture)),
}

View File

@ -19,13 +19,12 @@ use tokio::{
};
use tracing::{debug, warn};
use super::{Image, ImageError, load_image};
use super::{Image, ImageDecoderSource, ImageError};
use crate::{
spawn_tokio,
utils::{
File,
media::{FrameDimensions, MediaFileError},
save_data_to_tmp_file,
},
};
@ -156,7 +155,7 @@ impl ImageRequestQueueInner {
}
/// Add the given request to the queue.
fn add_request(&mut self, request_id: ImageRequestId, request: ImageRequest) {
fn queue_request(&mut self, request_id: ImageRequestId, request: ImageRequest) {
let is_limit_reached = self.is_limit_reached();
if !is_limit_reached || request.priority == ImageRequestPriority::High {
// Spawn the request right away.
@ -175,6 +174,31 @@ impl ImageRequestQueueInner {
self.requests.insert(request_id, request);
}
/// Add the given image request.
///
/// If another request for the same image already exists, this will reuse
/// the same request.
fn add_request(
&mut self,
inner: ImageLoaderRequest,
priority: ImageRequestPriority,
) -> ImageRequestHandle {
let request_id = inner.source.request_id();
// If the request already exists, use the existing one.
if let Some(request) = self.requests.get(&request_id) {
let result_receiver = request.result_sender.subscribe();
return ImageRequestHandle::new(result_receiver);
}
// Build and add the request.
let (request, result_receiver) = ImageRequest::new(inner, priority);
self.queue_request(request_id.clone(), request);
ImageRequestHandle::new(result_receiver)
}
/// Add a request to download an image.
///
/// If another request for the same image already exists, this will reuse
@ -186,24 +210,13 @@ impl ImageRequestQueueInner {
dimensions: Option<FrameDimensions>,
priority: ImageRequestPriority,
) -> ImageRequestHandle {
let data = DownloadRequestData {
client,
settings,
self.add_request(
ImageLoaderRequest {
source: ImageRequestSource::Download(DownloadRequest { client, settings }),
dimensions,
};
let request_id = data.request_id();
// If the request already exists, use the existing one.
if let Some(request) = self.requests.get(&request_id) {
let result_receiver = request.result_sender.subscribe();
return ImageRequestHandle::new(result_receiver);
}
// Build and add the request.
let (request, result_receiver) = ImageRequest::new(data, priority);
self.add_request(request_id.clone(), request);
ImageRequestHandle::new(result_receiver)
},
priority,
)
}
/// Add a request to load an image from a file.
@ -215,23 +228,15 @@ impl ImageRequestQueueInner {
file: File,
dimensions: Option<FrameDimensions>,
) -> ImageRequestHandle {
let data = FileRequestData { file, dimensions };
let request_id = data.request_id();
// If the request already exists, use the existing one.
if let Some(request) = self.requests.get(&request_id) {
let result_receiver = request.result_sender.subscribe();
return ImageRequestHandle::new(result_receiver);
}
// Build and add the request.
// Always use high priority because file requests should always be for
// previewing a local image.
let (request, result_receiver) = ImageRequest::new(data, ImageRequestPriority::High);
self.add_request(request_id.clone(), request);
ImageRequestHandle::new(result_receiver)
self.add_request(
ImageLoaderRequest {
source: ImageRequestSource::File(file),
dimensions,
},
ImageRequestPriority::High,
)
}
/// Mark the request with the given ID as stalled.
@ -335,8 +340,8 @@ impl ImageRequestQueueInner {
/// A request for an image.
struct ImageRequest {
/// The data of the request.
data: ImageRequestData,
/// The request to the image loader.
inner: ImageLoaderRequest,
/// The priority of the request.
priority: ImageRequestPriority,
/// The sender of the channel to use to send the result.
@ -352,13 +357,13 @@ struct ImageRequest {
impl ImageRequest {
/// Construct an image request with the given data and priority.
fn new(
data: impl Into<ImageRequestData>,
inner: ImageLoaderRequest,
priority: ImageRequestPriority,
) -> (Self, broadcast::Receiver<Result<Image, ImageError>>) {
let (result_sender, result_receiver) = broadcast::channel(1);
(
Self {
data: data.into(),
inner,
priority,
result_sender,
retries_count: 0,
@ -379,14 +384,14 @@ impl ImageRequest {
/// Spawn this request.
fn spawn(&self) {
let data = self.data.clone();
let inner = self.inner.clone();
let result_sender = self.result_sender.clone();
let retries_count = self.retries_count;
let task_handle = self.task_handle.clone();
let stalled_timeout_source = self.stalled_timeout_source.clone();
let abort_handle = spawn_tokio!(async move {
let request_id = data.request_id();
let request_id = inner.source.request_id();
let stalled_timeout_source_clone = stalled_timeout_source.clone();
let request_id_clone = request_id.clone();
@ -404,7 +409,7 @@ impl ImageRequest {
source.remove();
}
let result = data.await;
let result = inner.await;
// Cancel the timeout.
if let Ok(Some(source)) = stalled_timeout_source.lock().map(|mut s| s.take()) {
@ -451,7 +456,7 @@ impl Drop for ImageRequest {
handle.abort();
// Broadcast that the request was aborted.
let request_id = self.data.request_id();
let request_id = self.inner.source.request_id();
let result_sender = self.result_sender.clone();
spawn_tokio!(async move {
if let Err(error) = result_sender.send(Err(ImageError::Aborted)) {
@ -462,26 +467,17 @@ impl Drop for ImageRequest {
}
}
/// The data of a request to download an image.
/// A request to download an image.
#[derive(Clone)]
struct DownloadRequestData {
struct DownloadRequest {
/// The Matrix client to use to make the request.
client: Client,
/// The settings of the request.
settings: MediaRequestParameters,
/// The dimensions to request.
dimensions: Option<FrameDimensions>,
}
impl DownloadRequestData {
/// The ID of the image request with this data.
fn request_id(&self) -> ImageRequestId {
ImageRequestId::Download(self.settings.unique_key())
}
}
impl IntoFuture for DownloadRequestData {
type Output = Result<File, MediaFileError>;
impl IntoFuture for DownloadRequest {
type Output = Result<ImageDecoderSource, MediaFileError>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
@ -491,155 +487,76 @@ impl IntoFuture for DownloadRequestData {
Box::pin(async move {
let media = client.media();
let data = match media.get_media_content(&settings, true).await {
Ok(data) => data,
Err(error) => {
return Err(MediaFileError::from(error));
}
};
let data = media
.get_media_content(&settings, true)
.await
.map_err(MediaFileError::from)?;
let file = ImageDecoderSource::with_bytes(data).await?;
let file = save_data_to_tmp_file(data).await?;
Ok(file)
})
}
}
/// The data of a request to load an image file into a paintable.
/// A request to the image loader.
#[derive(Clone)]
struct FileRequestData {
/// The image file to load.
file: File,
struct ImageLoaderRequest {
/// The source of the image data.
source: ImageRequestSource,
/// The dimensions to request.
dimensions: Option<FrameDimensions>,
}
impl FileRequestData {
/// The ID of the image request with this data.
fn request_id(&self) -> ImageRequestId {
ImageRequestId::File(self.file.path().expect("file has a path"))
}
}
impl IntoFuture for FileRequestData {
type Output = Result<Image, glycin::ErrorCtx>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
let Self { file, dimensions } = self;
Box::pin(async move { load_image(file, dimensions).await })
}
}
/// The data of an image request.
#[derive(Clone)]
enum ImageRequestData {
/// The data for a download request.
Download {
/// The data to download the image.
download_data: DownloadRequestData,
/// The data to load the image into a paintable, after it was
/// downloaded.
file_data: Option<FileRequestData>,
},
/// The data for a file request.
File(FileRequestData),
}
impl ImageRequestData {
/// The ID of the image request with this data.
fn request_id(&self) -> ImageRequestId {
match self {
ImageRequestData::Download { download_data, .. } => download_data.request_id(),
ImageRequestData::File(file_data) => file_data.request_id(),
}
}
/// The data for the next request with this image request data.
fn into_next_request_data(self) -> DownloadOrFileRequestData {
match self {
Self::Download {
download_data,
file_data,
} => {
if let Some(file_data) = file_data {
file_data.into()
} else {
download_data.into()
}
}
Self::File(file_data) => file_data.into(),
}
}
}
impl IntoFuture for ImageRequestData {
impl IntoFuture for ImageLoaderRequest {
type Output = Result<Image, ImageError>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let file_data = match self.into_next_request_data() {
DownloadOrFileRequestData::Download(download_data) => {
let dimensions = download_data.dimensions;
// Load the data from the source.
let source = self.source.try_into_decoder_source().await?;
// Download the image to a file.
match download_data.await {
Ok(file) => FileRequestData { file, dimensions },
Err(error) => {
warn!("Could not retrieve image: {error}");
return Err(error.into());
}
}
}
DownloadOrFileRequestData::File(file_data) => file_data,
};
// Load the image from the file.
match file_data.clone().await {
Ok(image) => Ok(image),
Err(error) => {
warn!("Could not load image from file: {error}");
Err(error.into())
}
}
// Decode the image from the data.
source
.decode_image(self.dimensions)
.await
.inspect_err(|error| warn!("Could not decode image: {error}"))
})
}
}
impl From<DownloadRequestData> for ImageRequestData {
fn from(download_data: DownloadRequestData) -> Self {
Self::Download {
download_data,
file_data: None,
}
}
}
impl From<FileRequestData> for ImageRequestData {
fn from(value: FileRequestData) -> Self {
Self::File(value)
}
}
/// The data of a download request or a file request.
/// The source for an image request.
#[derive(Clone)]
enum DownloadOrFileRequestData {
/// The data for a download request.
Download(DownloadRequestData),
/// The data for a file request.
File(FileRequestData),
enum ImageRequestSource {
/// The image must be downloaded from the media cache or the server.
Download(DownloadRequest),
/// The image is in the given file.
File(File),
}
impl From<DownloadRequestData> for DownloadOrFileRequestData {
fn from(download_data: DownloadRequestData) -> Self {
Self::Download(download_data)
impl ImageRequestSource {
/// The ID of the image request with this source.
fn request_id(&self) -> ImageRequestId {
match self {
Self::Download(download_request) => {
ImageRequestId::Download(download_request.settings.unique_key())
}
Self::File(file) => ImageRequestId::File(file.path().expect("file should have a path")),
}
}
impl From<FileRequestData> for DownloadOrFileRequestData {
fn from(value: FileRequestData) -> Self {
Self::File(value)
/// Try to download the image, if necessary.
async fn try_into_decoder_source(self) -> Result<ImageDecoderSource, ImageError> {
match self {
Self::Download(download_request) => {
// Download the image.
Ok(download_request
.await
.inspect_err(|error| warn!("Could not retrieve image: {error}"))?)
}
Self::File(data) => Ok(data.into()),
}
}
}

View File

@ -3,8 +3,9 @@
use std::{
borrow::Cow,
cell::{Cell, OnceCell, RefCell},
fmt, fs, io,
io::Write,
fmt, fs,
io::{self, Write},
ops::Deref,
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::{Arc, LazyLock},
@ -384,36 +385,30 @@ impl<T> AsyncAction<T> {
}
}
/// A type that requires the tokio runtime to be running when dropped.
///
/// This is basically usable as a [`OnceCell`].
/// A wrapper that requires the tokio runtime to be running when dropped.
#[derive(Debug, Clone)]
pub struct TokioDrop<T>(OnceCell<T>);
pub struct TokioDrop<T>(Option<T>);
impl<T> TokioDrop<T> {
/// Create a new empty `TokioDrop`;
pub fn new() -> Self {
Self::default()
}
/// Gets a reference to the underlying value.
///
/// Returns `None` if the cell is empty.
pub fn get(&self) -> Option<&T> {
self.0.get()
}
/// Sets the contents of this cell to `value`.
///
/// Returns `Ok(())` if the cell was empty and `Err(value)` if it was full.
pub(crate) fn set(&self, value: T) -> Result<(), T> {
self.0.set(value)
/// Create a new `TokioDrop` wrapping the given type.
pub fn new(value: T) -> Self {
Self(Some(value))
}
}
impl<T> Default for TokioDrop<T> {
fn default() -> Self {
Self(Default::default())
impl<T> Deref for TokioDrop<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0
.as_ref()
.expect("TokioDrop should always contain a value")
}
}
impl<T> From<T> for TokioDrop<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
@ -421,35 +416,12 @@ impl<T> Drop for TokioDrop<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
if let Some(inner) = self.0.take() {
drop(inner);
if let Some(value) = self.0.take() {
drop(value);
}
}
}
impl<T: glib::property::Property> glib::property::Property for TokioDrop<T> {
type Value = T::Value;
}
impl<T> glib::property::PropertyGet for TokioDrop<T> {
type Value = T;
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
f(self.get().unwrap())
}
}
impl<T> glib::property::PropertySet for TokioDrop<T> {
type SetValue = T;
fn set(&self, v: Self::SetValue) {
assert!(
self.set(v).is_ok(),
"TokioDrop value was already initialized"
);
}
}
/// The state of a resource that can be loaded.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
#[enum_type(name = "LoadingState")]
@ -539,6 +511,7 @@ pub(crate) async fn save_data_to_tmp_file(data: Vec<u8>) -> Result<File, std::io
}
}
let mut file = NamedTempFile::new_in(dir)?;
tracing::debug!("Created new tmp file: {}", file.path().to_string_lossy());
file.write_all(&data)?;
Ok(file.into())