Compare commits

...

33 Commits

Author SHA1 Message Date
Krille-chan
b4fcb5b0d9 chore: Use correct versions of adaptive_dialog 2023-07-14 08:02:47 +00:00
Krille
1911004d05
refactor: Update dependencies 2023-07-13 19:46:16 +09:00
Raatty
5d67564445
Added translation using Weblate (Greek) 2023-07-09 09:42:27 +02:00
Krille
be04c5a46e
design: Adjust open url dialog design a little bit 2023-07-07 12:10:07 +09:00
Farooq Karimi Zadeh
bd7a4c9dfb
Translated using Weblate (Persian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fa/
2023-07-06 10:52:33 +02:00
Krille
10ee57722e
chore: Enable webrtc for linux again 2023-06-30 19:15:45 +09:00
Malin Errenst
ff5f7ab50e Merge branch 'malin/group-notification-channels' into 'main'
feat: Added grouping to message notification channels

See merge request famedly/fluffychat!1134
2023-06-30 07:55:24 +00:00
Krille
277885a61e
chore: Streamline background gradients 2023-06-29 16:56:15 +09:00
Malin Errenst
6633ebc376
feat: Added grouping to message notification channels 2023-06-29 09:36:08 +02:00
Krille-chan
b2d9986cd3 Merge branch 'braid/cute-events' into 'main'
fix: overflow in cute events

See merge request famedly/fluffychat!1132
2023-06-29 07:13:55 +00:00
Krille-chan
a0b9bb277f Merge branch 'braid/url-launch-copy' into 'main'
feat: add button to copy url in open dialog

See merge request famedly/fluffychat!1133
2023-06-29 07:13:30 +00:00
TheOneWithTheBraid
d381705cdd
feat: add button to copy url in open dialog
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-06-27 14:09:00 +02:00
3820d4264a
Translated using Weblate (Finnish)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fi/
2023-06-26 14:50:25 +02:00
Milo Ivir
cf4e2d3fad
Translated using Weblate (Croatian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/hr/
2023-06-26 14:50:24 +02:00
josé m
002dc87577
Translated using Weblate (Galician)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/
2023-06-22 07:51:05 +02:00
The one with the braid
922e7ad0ff fix: overflow in cute events
Signed-off-by: The one with the braid <the-one@with-the-braid.cf>
2023-06-21 16:08:20 +02:00
Oğuz Ersen
5d7be8a672
Translated using Weblate (Turkish)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2023-06-20 19:50:49 +02:00
Krille-chan
431b357cfa Merge branch 'braid/allow-aarch64-failure' into 'main'
fix: allow aarch64 upload failure

See merge request famedly/fluffychat!1131
2023-06-19 10:35:58 +00:00
TheOneWithTheBraid
2938acf152 fix: allow aarch64 upload failure
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-06-19 12:27:07 +02:00
Krille
4127f70e4d
build: Change wakelock hotfix 2023-06-19 10:48:10 +02:00
xabirequejo
c07221cc12
Translated using Weblate (Basque)
Currently translated at 97.6% (536 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/
2023-06-17 20:51:13 +02:00
Rex_sa
33b2f95e3f
Translated using Weblate (Arabic)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/
2023-06-16 11:52:28 +02:00
Ihor Hordiichuk
2a5cd9b218
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2023-06-15 10:51:43 +02:00
Linerly
a15fed034d
Translated using Weblate (Indonesian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2023-06-13 14:47:59 +02:00
Riley
b9641ac021
Translated using Weblate (Romanian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ro/
2023-06-13 14:47:59 +02:00
josé m
2145bd8846
Translated using Weblate (Galician)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/
2023-06-13 14:47:58 +02:00
Priit Jõerüüt
672b97b310
Translated using Weblate (Estonian)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/
2023-06-13 14:47:58 +02:00
Umoya NgoLwesihlanu
974da6ec90
Translated using Weblate (Arabic)
Currently translated at 100.0% (549 of 549 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ar/
2023-06-13 14:47:57 +02:00
Krille
f19bbcd010
refactor: More reliable request history/future timeline mechanism 2023-06-13 08:41:49 +02:00
Krille
a1468c92c8 Merge branch 'braid/allow-windows-failure' into 'main'
fix: allow windows upload job failure

See merge request famedly/fluffychat!1129
2023-06-12 05:19:41 +00:00
TheOneWithTheBraid
6dd125a087 fix: allow windows upload job failure
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-06-11 19:46:11 +02:00
Krille
842ecc4235
feat: New simplified login process with more prominent SSO and nicer layout 2023-06-11 18:04:31 +02:00
Krille
db66793d28
docs: Update mastodon link 2023-06-11 10:56:23 +02:00
53 changed files with 11058 additions and 11267 deletions

View File

@ -372,6 +372,7 @@ upload_linux_arm64:
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
allow_failure: true
upload_windows:
extends: .release
@ -383,6 +384,7 @@ upload_windows:
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
allow_failure: true
deploy_playstore:
stage: deploy

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -2252,9 +2252,9 @@
"@hydrateTor": {},
"commandHint_googly": "أرسل بعض عيون googly",
"@commandHint_googly": {},
"commandHint_cuddle": "إرسال عناق",
"commandHint_cuddle": "أرسل عناق",
"@commandHint_cuddle": {},
"commandHint_hug": "إرسال عناق",
"commandHint_hug": "إرسال حضن",
"@commandHint_hug": {},
"cuddleContent": "{senderName} يحتضنك",
"@cuddleContent": {
@ -2504,5 +2504,11 @@
"jumpToLastReadMessage": "الانتقال إلى آخر رسالة مقروءة",
"@jumpToLastReadMessage": {},
"readUpToHere": "اقرأ حتى هنا",
"@readUpToHere": {}
"@readUpToHere": {},
"signInWithPassword": "سجل الدخول بكلمة السر",
"@signInWithPassword": {},
"continueWith": "أكمل ب:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "رجاء حاول مجددا أو اختر خادما مختلفا.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
}

1
assets/l10n/intl_el.arb Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -2473,5 +2473,8 @@
"jump": "Jump",
"openLinkInBrowser": "Open link in browser",
"reportErrorDescription": "Oh no. Something went wrong. Please try again later. If you want, you can report the bug to the developers.",
"report": "report"
"report": "report",
"signInWithPassword": "Sign in with password",
"continueWith": "Continue with:",
"pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server."
}

View File

@ -2504,5 +2504,11 @@
"placeholders": {}
},
"reportErrorDescription": "Oh appike! Midagi läks valesti. Palun proovi hiljem uuesti. Kui soovid, võid sellest veast arendajatele teatada.",
"@reportErrorDescription": {}
"@reportErrorDescription": {},
"continueWith": "Jätkamiseks kasuta:",
"@continueWith": {},
"signInWithPassword": "Logi sisse salasõnaga",
"@signInWithPassword": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Palun proovi hiljem uuesti või muuda serveri nime.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@
"type": "text",
"placeholders": {}
},
"activatedEndToEndEncryption": "🔐 {username} activou o cifrado extremo-a-extremo",
"activatedEndToEndEncryption": "🔐 {username} activou a cifraxe extremo-a-extremo",
"@activatedEndToEndEncryption": {
"type": "text",
"placeholders": {
@ -314,7 +314,7 @@
"type": "text",
"placeholders": {}
},
"channelCorruptedDecryptError": "O cifrado está corrompido",
"channelCorruptedDecryptError": "A cifraxe está estragada",
"@channelCorruptedDecryptError": {
"type": "text",
"placeholders": {}
@ -704,12 +704,12 @@
"type": "text",
"placeholders": {}
},
"enableEncryption": "Activar cifrado",
"enableEncryption": "Activar cifraxe",
"@enableEncryption": {
"type": "text",
"placeholders": {}
},
"enableEncryptionWarning": "Non poderás desactivar o cifrado posteriormente, tes certeza?",
"enableEncryptionWarning": "Non poderás desactivar a cifraxe posteriormente, tes certeza?",
"@enableEncryptionWarning": {
"type": "text",
"placeholders": {}
@ -719,12 +719,12 @@
"type": "text",
"placeholders": {}
},
"encryption": "Cifrado",
"encryption": "Cifraxe",
"@encryption": {
"type": "text",
"placeholders": {}
},
"encryptionNotEnabled": "O cifrado non está activado",
"encryptionNotEnabled": "A cifraxe non está activada",
"@encryptionNotEnabled": {
"type": "text",
"placeholders": {}
@ -1119,7 +1119,7 @@
"type": "text",
"placeholders": {}
},
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para o cifrado extremo-a-extremo.",
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para a cifraxe extremo-a-extremo.",
"@needPantalaimonWarning": {
"type": "text",
"placeholders": {}
@ -1159,7 +1159,7 @@
"type": "text",
"placeholders": {}
},
"noEncryptionForPublicRooms": "Só podes activar o cifrado tan pronto como a sala non sexa públicamente accesible.",
"noEncryptionForPublicRooms": "Só podes activar a cifraxe tan pronto como a sala non sexa públicamente accesible.",
"@noEncryptionForPublicRooms": {
"type": "text",
"placeholders": {}
@ -1824,7 +1824,7 @@
"type": "text",
"placeholders": {}
},
"unknownEncryptionAlgorithm": "Algoritmo de cifrado descoñecido",
"unknownEncryptionAlgorithm": "Algoritmo de cifraxe descoñecido",
"@unknownEncryptionAlgorithm": {
"type": "text",
"placeholders": {}
@ -2103,7 +2103,7 @@
"type": "text",
"description": "Usage hint for the command /discardsession"
},
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar o cifrado",
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar a cifraxe",
"@commandHint_create": {
"type": "text",
"description": "Usage hint for the command /create"
@ -2113,7 +2113,7 @@
"type": "text",
"description": "Usage hint for the command /clearcache"
},
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar o cifrado",
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar a cifraxe",
"@commandHint_dm": {
"type": "text",
"description": "Usage hint for the command /dm"
@ -2447,9 +2447,9 @@
"@enterInviteLinkOrMatrixId": {},
"encryptThisChat": "Cifrar esta conversa",
"@encryptThisChat": {},
"endToEndEncryption": "Cifrado de extremo a extremo",
"endToEndEncryption": "Cifraxe de extremo a extremo",
"@endToEndEncryption": {},
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar o cifrado dunha conversa onde foi activado previamente.",
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar a cifraxe dunha conversa onde foi activada previamente.",
"@disableEncryptionWarning": {},
"sorryThatsNotPossible": "Lamentámolo... iso non é posible",
"@sorryThatsNotPossible": {},
@ -2504,5 +2504,11 @@
"@discover": {
"type": "text",
"placeholders": {}
}
},
"signInWithPassword": "Accede con contrasinal",
"@signInWithPassword": {},
"continueWith": "Continuar con:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Inténtao máis tarde ou elixe un servidor diferente.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
}

View File

@ -2503,6 +2503,12 @@
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš grešku možeš prijaviti programerima.",
"@reportErrorDescription": {}
"reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš, grešku možeš prijaviti programerima.",
"@reportErrorDescription": {},
"signInWithPassword": "Prijavi se s lozinkom",
"@signInWithPassword": {},
"continueWith": "Nastavi sa:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Pokušaj ponovo kasnije ili odaberi jedan drugi poslužitelj.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
}

View File

@ -2503,5 +2503,11 @@
"report": "laporkan",
"@report": {},
"reportErrorDescription": "Aduh. Ada yang salah. Silakan coba lahi nanti. Jika kamu mau, kamu bisa melaporkan kutu ini kepada para pengembang.",
"@reportErrorDescription": {}
"@reportErrorDescription": {},
"signInWithPassword": "Masuk dengan kata sandi",
"@signInWithPassword": {},
"continueWith": "Lanjutkan dengan:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Silakan coba lagi nanti atau pilih server yang lain.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
}

View File

@ -2496,5 +2496,11 @@
"placeholders": {}
},
"reopenChat": "Deschide din nou chatul",
"@reopenChat": {}
"@reopenChat": {},
"continueWith": "Continuați cu:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Vă rugăm să încercați din nou mai târziu sau să alegeți un server diferit.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
"signInWithPassword": "Conectați-vă cu parolă",
"@signInWithPassword": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -2504,5 +2504,11 @@
"@discover": {
"type": "text",
"placeholders": {}
}
},
"pleaseTryAgainLaterOrChooseDifferentServer": "Спробуйте пізніше або виберіть інший сервер.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
"signInWithPassword": "Увійти за допомогою пароля",
"@signInWithPassword": {},
"continueWith": "Продовжити за допомогою:",
"@continueWith": {}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -55,7 +55,7 @@
<div class="flex mb-8 justify-center content-center">
<a rel="me"
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
rel="me" href="https://metalhead.club/@krille">
rel="me" href="https://mastodon.art/@krille">
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
@ -116,4 +116,4 @@
</body>
</html>
</html>

View File

@ -9,7 +9,6 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/pages/device_settings/device_settings.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
@ -27,7 +26,6 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
import 'package:fluffychat/pages/settings_security/settings_security.dart';
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pages/sign_up/signup.dart';
import 'package:fluffychat/pages/story/story_page.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/loading_view.dart';
@ -266,23 +264,6 @@ class AppRoutes {
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'connect',
widget: const ConnectPage(),
buildTransition: _fadeTransition,
stackedRoutes: [
VWidget(
path: 'login',
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'signup',
widget: const SignupPage(),
buildTransition: _fadeTransition,
),
],
),
VWidget(
path: 'logs',
widget: const LogViewer(),
@ -358,23 +339,6 @@ class AppRoutes {
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'connect',
widget: const ConnectPage(),
buildTransition: _fadeTransition,
stackedRoutes: [
VWidget(
path: 'login',
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'signup',
widget: const SignupPage(),
buildTransition: _fadeTransition,
),
],
),
],
),
VWidget(

View File

@ -41,6 +41,22 @@ abstract class FluffyThemes {
titleSmall: fallbackTextStyle,
);
static LinearGradient backgroundGradient(
BuildContext context,
int alpha,
) {
final colorScheme = Theme.of(context).colorScheme;
return LinearGradient(
begin: Alignment.topCenter,
colors: [
colorScheme.primaryContainer.withAlpha(alpha),
colorScheme.secondaryContainer.withAlpha(alpha),
colorScheme.tertiaryContainer.withAlpha(alpha),
colorScheme.primaryContainer.withAlpha(alpha),
],
);
}
static const Duration animationDuration = Duration(milliseconds: 250);
static const Curve animationCurve = Curves.easeInOut;

View File

@ -204,6 +204,7 @@ class ChatController extends State<ChatPageWithRoom> {
void requestHistory() async {
if (!timeline!.canRequestHistory) return;
Logs().v('Requesting history...');
try {
await timeline!.requestHistory(historyCount: _loadHistoryCount);
} catch (err) {
@ -222,6 +223,7 @@ class ChatController extends State<ChatPageWithRoom> {
final timeline = this.timeline;
if (timeline == null) return;
if (!timeline.canRequestFuture) return;
Logs().v('Requesting future...');
try {
final mostRecentEventId = timeline.events.first.eventId;
await timeline.requestFuture(historyCount: _loadHistoryCount);
@ -244,12 +246,6 @@ class ChatController extends State<ChatPageWithRoom> {
}
setReadMarker();
if (!scrollController.hasClients) return;
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
requestHistory();
} else if (scrollController.position.pixels == 0) {
requestFuture();
}
if (timeline?.allowNewEvent == false ||
scrollController.position.pixels > 0 && _scrolledUp == false) {
setState(() => _scrolledUp = true);
@ -873,8 +869,11 @@ class ChatController extends State<ChatPageWithRoom> {
setState(() => showEmojiPicker = false);
if (emoji == null) return;
// make sure we don't send the same emoji twice
if (_allReactionEvents
.any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return;
if (_allReactionEvents.any(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
)) {
return;
}
return sendEmojiAction(emoji.emoji);
}

View File

@ -53,11 +53,18 @@ class ChatEventList extends StatelessWidget {
);
}
if (controller.timeline!.canRequestFuture) {
return Center(
child: IconButton(
onPressed: controller.requestFuture,
icon: const Icon(Icons.refresh_outlined),
),
return Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => controller.requestFuture(),
);
return Center(
child: IconButton(
onPressed: controller.requestFuture,
icon: const Icon(Icons.refresh_outlined),
),
);
},
);
}
return Column(
@ -77,11 +84,18 @@ class ChatEventList extends StatelessWidget {
);
}
if (controller.timeline!.canRequestHistory) {
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
return Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => controller.requestHistory(),
);
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
);
},
);
}
return const SizedBox.shrink();

View File

@ -146,7 +146,6 @@ class ChatView extends StatelessWidget {
);
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
final colorScheme = Theme.of(context).colorScheme;
return VWidgetGuard(
onSystemPop: (redirector) async {
@ -220,14 +219,9 @@ class ChatView extends StatelessWidget {
else
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
colors: [
colorScheme.primaryContainer.withAlpha(64),
colorScheme.secondaryContainer.withAlpha(64),
colorScheme.tertiaryContainer.withAlpha(64),
colorScheme.primaryContainer.withAlpha(64),
],
gradient: FluffyThemes.backgroundGradient(
context,
64,
),
),
),

View File

@ -36,19 +36,16 @@ class _CuteContentState extends State<CuteContent> {
return GestureDetector(
onTap: addOverlay,
child: SizedBox.square(
dimension: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.event.text,
style: const TextStyle(fontSize: 150),
),
if (label != null) Text(label)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.event.text,
style: const TextStyle(fontSize: 150),
),
if (label != null) Text(label)
],
),
);
},
@ -144,24 +141,26 @@ class _CuteEventOverlayState extends State<CuteEventOverlay>
return SizedBox(
height: constraints.maxHeight,
width: constraints.maxWidth,
child: Stack(
alignment: Alignment.bottomLeft,
fit: StackFit.expand,
children: items
.map(
(position) => Positioned(
left: position.width * width,
bottom: (height *
.25 *
position.height *
(controller?.value ?? 0)) -
_CuteOverlayContent.size,
child: _CuteOverlayContent(
emoji: widget.emoji,
child: OverflowBox(
child: Stack(
alignment: Alignment.bottomLeft,
fit: StackFit.expand,
children: items
.map(
(position) => Positioned(
left: position.width * width,
bottom: (height *
.25 *
position.height *
(controller?.value ?? 0)) -
_CuteOverlayContent.size,
child: _CuteOverlayContent(
emoji: widget.emoji,
),
),
),
)
.toList(),
)
.toList(),
),
),
);
},

View File

@ -61,7 +61,7 @@ class MessageReactions extends StatelessWidget {
final evt = allReactionEvents.firstWhereOrNull(
(e) =>
e.senderId == e.room.client.userID &&
e.content['m.relates_to']['key'] == r.key,
e.content.tryGetMap('m.relates_to')?['key'] == r.key,
);
if (evt != null) {
showFutureLoadingDialog(

View File

@ -52,7 +52,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
final networkUri = _networkUri;
if (kIsWeb && networkUri != null && _chewieManager == null) {
_chewieManager ??= ChewieController(
videoPlayerController: VideoPlayerController.network(networkUri),
videoPlayerController:
VideoPlayerController.networkUrl(Uri.parse(networkUri)),
autoPlay: true,
autoInitialize: true,
);

View File

@ -183,12 +183,13 @@ class InputBar extends StatelessWidget {
final state = r.getState(EventTypes.RoomCanonicalAlias);
if ((state != null &&
((state.content['alias'] is String &&
state.content['alias']
state.content
.tryGet<String>('alias')!
.split(':')[0]
.toLowerCase()
.contains(roomSearch)) ||
(state.content['alt_aliases'] is List &&
state.content['alt_aliases'].any(
(state.content['alt_aliases'] as List).any(
(l) =>
l is String &&
l

View File

@ -52,7 +52,7 @@ class ReactionsPicker extends StatelessWidget {
for (final event in allReactionEvents) {
try {
emojis.remove(event.content['m.relates_to']['key']);
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
} catch (_) {}
}
return Row(

View File

@ -83,7 +83,9 @@ class ChatDetailsController extends State<ChatDetails> {
RequestType.GET,
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
)
.then((response) => List<String>.from(response['aliases'])),
.then(
(response) => List<String>.from(response['aliases'] as Iterable),
),
);
// Switch to the stable api once it is implemented.

View File

@ -73,8 +73,10 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
void updateRoomAction(Capabilities capabilities) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final String roomVersion =
room.getState(EventTypes.RoomCreate)!.content['room_version'] ?? '1';
final roomVersion = room
.getState(EventTypes.RoomCreate)!
.content['room_version'] as String? ??
'1';
final newVersion = await showConfirmationDialog<String>(
context: context,
title: L10n.of(context)!.replaceRoomWithNewerVersion,

View File

@ -127,9 +127,9 @@ class ChatPermissionsSettingsView extends StatelessWidget {
),
);
}
final String roomVersion = room
final roomVersion = room
.getState(EventTypes.RoomCreate)!
.content['room_version'] ??
.content['room_version'] as String? ??
'1';
return ListTile(

View File

@ -1,197 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/connect/connect_page_view.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ConnectPage extends StatefulWidget {
const ConnectPage({Key? key}) : super(key: key);
@override
State<ConnectPage> createState() => ConnectPageController();
}
class ConnectPageController extends State<ConnectPage> {
final TextEditingController usernameController = TextEditingController();
String? signupError;
bool loading = false;
void pickAvatar() async {
final source = !PlatformInfos.isMobile
? ImageSource.gallery
: await showModalActionSheet<ImageSource>(
context: context,
title: L10n.of(context)!.changeYourAvatar,
actions: [
SheetAction(
key: ImageSource.camera,
label: L10n.of(context)!.openCamera,
isDefaultAction: true,
icon: Icons.camera_alt_outlined,
),
SheetAction(
key: ImageSource.gallery,
label: L10n.of(context)!.openGallery,
icon: Icons.photo_outlined,
),
],
);
if (source == null) return;
final picked = await ImagePicker().pickImage(
source: source,
imageQuality: 50,
maxWidth: 512,
maxHeight: 512,
);
setState(() {
Matrix.of(context).loginAvatar = picked;
});
}
void signUp() async {
usernameController.text = usernameController.text.trim();
final localpart =
usernameController.text.toLowerCase().replaceAll(' ', '_');
if (localpart.isEmpty) {
setState(() {
signupError = L10n.of(context)!.pleaseChooseAUsername;
});
return;
}
setState(() {
signupError = null;
loading = true;
});
try {
try {
await Matrix.of(context).getLoginClient().register(username: localpart);
} on MatrixException catch (e) {
if (!e.requireAdditionalAuthentication) rethrow;
}
setState(() {
loading = false;
});
Matrix.of(context).loginUsername = usernameController.text;
VRouter.of(context).to('signup');
} catch (e, s) {
Logs().d('Sign up failed', e, s);
setState(() {
signupError = e.toLocalizedString(context);
loading = false;
});
}
}
bool _supportsFlow(String flowType) =>
Matrix.of(context)
.loginHomeserverSummary
?.loginFlows
.any((flow) => flow.type == flowType) ??
false;
bool get supportsSso => _supportsFlow('m.login.sso');
bool isDefaultPlatform =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
bool get supportsLogin => _supportsFlow('m.login.password');
void login() => VRouter.of(context).to('login');
Map<String, dynamic>? _rawLoginTypes;
List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null;
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
(flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'];
final list = (rawProviders as List)
.map((json) => IdentityProvider.fromJson(json))
.toList();
if (PlatformInfos.isCupertinoStyle) {
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
}
return list;
}
void ssoLoginAction(String id) async {
final redirectUrl = kIsWeb
? '${html.window.origin!}/web/auth.html'
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url =
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
final result = await FlutterWebAuth2.authenticate(
url: url,
callbackUrlScheme: urlScheme,
);
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).getLoginClient().login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
}
@override
void initState() {
super.initState();
if (supportsSso) {
Matrix.of(context)
.getLoginClient()
.request(
RequestType.GET,
'/client/r0/login',
)
.then(
(loginTypes) => setState(() {
_rawLoginTypes = loginTypes;
}),
);
}
}
@override
Widget build(BuildContext context) => ConnectPageView(this);
}
class IdentityProvider {
final String? id;
final String? name;
final String? icon;
final String? brand;
IdentityProvider({this.id, this.name, this.icon, this.brand});
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
IdentityProvider(
id: json['id'],
name: json['name'],
icon: json['icon'],
brand: json['brand'],
);
}

View File

@ -1,226 +0,0 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'sso_button.dart';
class ConnectPageView extends StatelessWidget {
final ConnectPageController controller;
const ConnectPageView(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final avatar = Matrix.of(context).loginAvatar;
final identityProviders = controller.identityProviders;
return LoginScaffold(
appBar: AppBar(
leading: controller.loading ? null : const BackButton(),
automaticallyImplyLeading: !controller.loading,
centerTitle: true,
title: Text(
Matrix.of(context).getLoginClient().homeserver?.host ?? '',
),
),
body: ListView(
key: const Key('ConnectPageListView'),
children: [
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: Stack(
children: [
Material(
borderRadius: BorderRadius.circular(64),
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
10,
color: Colors.transparent,
shadowColor: Theme.of(context)
.colorScheme
.onBackground
.withAlpha(64),
clipBehavior: Clip.hardEdge,
child: CircleAvatar(
radius: 64,
backgroundColor: Colors.white,
child: avatar == null
? const Icon(
Icons.person,
color: Colors.black,
size: 64,
)
: FutureBuilder<Uint8List>(
future: avatar.readAsBytes(),
builder: (context, snapshot) {
final bytes = snapshot.data;
if (bytes == null) {
return const CircularProgressIndicator
.adaptive();
}
return Image.memory(
bytes,
fit: BoxFit.cover,
width: 128,
height: 128,
);
},
),
),
),
Positioned(
bottom: 0,
right: 0,
child: FloatingActionButton(
mini: true,
onPressed: controller.pickAvatar,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
child: const Icon(Icons.camera_alt_outlined),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: controller.usernameController,
onSubmitted: (_) => controller.signUp(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_box_outlined),
hintText: L10n.of(context)!.chooseAUsername,
errorText: controller.signupError,
errorStyle: const TextStyle(color: Colors.orange),
),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: 'loginButton',
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading ? () {} : controller.signUp,
icon: const Icon(Icons.person_add_outlined),
label: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.signUp),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
],
if (controller.supportsSso)
identityProviders == null
? const SizedBox(
height: 74,
child: Center(child: CircularProgressIndicator.adaptive()),
)
: Center(
child: identityProviders.length == 1
? Container(
width: double.infinity,
padding: const EdgeInsets.all(12.0),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
icon: identityProviders.single.icon == null
? const Icon(
Icons.web_outlined,
size: 16,
)
: Image.network(
Uri.parse(identityProviders.single.icon!)
.getDownloadLink(
Matrix.of(context).getLoginClient(),
)
.toString(),
width: 32,
height: 32,
),
onPressed: () => controller
.ssoLoginAction(identityProviders.single.id!),
label: Text(
identityProviders.single.name ??
identityProviders.single.brand ??
L10n.of(context)!.loginWithOneClick,
),
),
)
: Wrap(
children: [
for (final identityProvider in identityProviders)
SsoButton(
onPressed: () => controller
.ssoLoginAction(identityProvider.id!),
identityProvider: identityProvider,
),
].toList(),
),
),
if (controller.supportsLogin)
Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: 'signinButton',
child: ElevatedButton.icon(
icon: const Icon(Icons.login_outlined),
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
),
onPressed: controller.loading ? () {} : controller.login,
label: Text(L10n.of(context)!.login),
),
),
),
],
),
);
}
}

View File

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SsoButton extends StatelessWidget {
final IdentityProvider identityProvider;
final void Function()? onPressed;
const SsoButton({
Key? key,
required this.identityProvider,
this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(7),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: identityProvider.icon == null
? const Icon(Icons.web_outlined)
: Image.network(
Uri.parse(identityProvider.icon!)
.getDownloadLink(
Matrix.of(context).getLoginClient(),
)
.toString(),
width: 32,
height: 32,
),
),
),
const SizedBox(height: 8),
Text(
identityProvider.name ??
identityProvider.brand ??
L10n.of(context)!.singlesignon,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:fluffychat/config/app_config.dart';
import 'homeserver_bottom_sheet.dart';
import 'homeserver_picker.dart';
class HomeserverAppBar extends StatelessWidget {
@ -13,25 +16,57 @@ class HomeserverAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextField(
focusNode: controller.homeserverFocusNode,
controller: controller.homeserverController,
onChanged: controller.onChanged,
decoration: InputDecoration(
prefixIcon: Navigator.of(context).canPop()
? IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.arrow_back),
)
: null,
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
errorText: controller.error,
return TypeAheadField<HomeserverBenchmarkResult>(
suggestionsBoxDecoration: SuggestionsBoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
constraints: const BoxConstraints(maxHeight: 256),
),
itemBuilder: (context, homeserver) => ListTile(
title: Text(homeserver.homeserver.baseUrl.toString()),
subtitle: Text(homeserver.homeserver.description ?? ''),
trailing: IconButton(
icon: const Icon(Icons.info_outlined),
onPressed: () => showModalBottomSheet(
context: context,
builder: (_) => HomeserverBottomSheet(
homeserver: homeserver,
),
),
),
),
suggestionsCallback: (pattern) async {
final homeserverList =
await const JoinmatrixOrgParser().fetchHomeservers();
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
homeserverList,
timeout: const Duration(seconds: 3),
);
return benchmark;
},
onSuggestionSelected: (suggestion) {
controller.homeserverController.text =
suggestion.homeserver.baseUrl.host;
controller.checkHomeserverAction();
},
textFieldConfiguration: TextFieldConfiguration(
controller: controller.homeserverController,
decoration: InputDecoration(
prefixIcon: Navigator.of(context).canPop()
? IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.arrow_back),
)
: null,
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
),
textInputAction: TextInputAction.search,
onSubmitted: controller.checkHomeserverAction,
autocorrect: false,
),
readOnly: !AppConfig.allowOtherHomeservers,
onSubmitted: (_) => controller.checkHomeserverAction(),
autocorrect: false,
);
}
}

View File

@ -7,16 +7,16 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:universal_html/html.dart' as html;
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_bottom_sheet.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/localized_exception_extension.dart';
@ -35,14 +35,8 @@ class HomeserverPickerController extends State<HomeserverPicker> {
final TextEditingController homeserverController = TextEditingController(
text: AppConfig.defaultHomeserver,
);
final FocusNode homeserverFocusNode = FocusNode();
String? error;
List<HomeserverBenchmarkResult>? benchmarkResults;
bool displayServerList = false;
bool get loadingHomeservers =>
AppConfig.allowOtherHomeservers && benchmarkResults == null;
String searchTerm = '';
String? error;
bool isTorBrowser = false;
@ -65,98 +59,34 @@ class HomeserverPickerController extends State<HomeserverPicker> {
isTorBrowser = isTor;
}
void _updateFocus() {
if (benchmarkResults == null) _loadHomeserverList();
if (homeserverFocusNode.hasFocus) {
setState(() {
displayServerList = true;
});
}
}
void showServerInfo(HomeserverBenchmarkResult server) =>
showAdaptiveBottomSheet(
context: context,
builder: (_) => HomeserverBottomSheet(
homeserver: server,
),
);
void onChanged(String text) => setState(() {
searchTerm = text;
});
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
.where(
(element) =>
element.homeserver.baseUrl.host.contains(searchTerm) ||
(element.homeserver.description?.contains(searchTerm) ?? false),
)
.toList();
void _loadHomeserverList() async {
try {
final homeserverList =
await const JoinmatrixOrgParser().fetchHomeservers();
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
homeserverList,
timeout: const Duration(seconds: 10),
);
if (!mounted) return;
setState(() {
benchmarkResults = benchmark;
});
} catch (e, s) {
Logs().e('Homeserver benchmark failed', e, s);
benchmarkResults = [];
}
}
void setServer(String server) => setState(() {
homeserverController.text = server;
searchTerm = '';
homeserverFocusNode.unfocus();
displayServerList = false;
});
String? _lastCheckedUrl;
/// Starts an analysis of the given homeserver. It uses the current domain and
/// makes sure that it is prefixed with https. Then it searches for the
/// well-known information and forwards to the login page depending on the
/// login type.
Future<void> checkHomeserverAction() async {
Future<void> checkHomeserverAction([_]) async {
homeserverController.text =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
if (homeserverController.text == _lastCheckedUrl) return;
_lastCheckedUrl = homeserverController.text;
setState(() {
homeserverFocusNode.unfocus();
error = null;
error = _rawLoginTypes = loginHomeserverSummary = null;
isLoading = true;
searchTerm = '';
displayServerList = false;
});
try {
homeserverController.text =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
var homeserver = Uri.parse(homeserverController.text);
if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeserverController.text, '');
}
final matrix = Matrix.of(context);
matrix.loginHomeserverSummary =
await matrix.getLoginClient().checkHomeserver(homeserver);
final ssoSupported = matrix.loginHomeserverSummary!.loginFlows
.any((flow) => flow.type == 'm.login.sso');
try {
await Matrix.of(context).getLoginClient().register();
matrix.loginRegistrationSupported = true;
} on MatrixException catch (e) {
matrix.loginRegistrationSupported = e.requireAdditionalAuthentication;
}
if (!ssoSupported && matrix.loginRegistrationSupported == false) {
// Server does not support SSO or registration. We can skip to login page:
VRouter.of(context).to('login');
} else {
VRouter.of(context).to('connect');
final client = Matrix.of(context).getLoginClient();
loginHomeserverSummary = await client.checkHomeserver(homeserver);
if (supportsSso) {
_rawLoginTypes = await client.request(
RequestType.GET,
'/client/r0/login',
);
}
} catch (e) {
setState(() => error = (e).toLocalizedString(context));
@ -167,17 +97,71 @@ class HomeserverPickerController extends State<HomeserverPicker> {
}
}
@override
void dispose() {
homeserverFocusNode.removeListener(_updateFocus);
super.dispose();
HomeserverSummary? loginHomeserverSummary;
bool _supportsFlow(String flowType) =>
loginHomeserverSummary?.loginFlows.any((flow) => flow.type == flowType) ??
false;
bool get supportsSso => _supportsFlow('m.login.sso');
bool isDefaultPlatform =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
bool get supportsPasswordLogin => _supportsFlow('m.login.password');
Map<String, dynamic>? _rawLoginTypes;
void ssoLoginAction(String id) async {
final redirectUrl = kIsWeb
? '${html.window.origin!}/web/auth.html'
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url =
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
final result = await FlutterWebAuth2.authenticate(
url: url,
callbackUrlScheme: urlScheme,
);
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).getLoginClient().login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
}
List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null;
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
(flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'];
final list = (rawProviders as List)
.map((json) => IdentityProvider.fromJson(json))
.toList();
if (PlatformInfos.isCupertinoStyle) {
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
}
return list;
}
void login() => VRouter.of(context).to('login');
@override
void initState() {
homeserverFocusNode.addListener(_updateFocus);
_checkTorBrowser();
super.initState();
WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction);
}
@override
@ -204,3 +188,20 @@ class HomeserverPickerController extends State<HomeserverPicker> {
);
}
}
class IdentityProvider {
final String? id;
final String? name;
final String? icon;
final String? brand;
IdentityProvider({this.id, this.name, this.icon, this.brand});
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
IdentityProvider(
id: json['id'],
name: json['name'],
icon: json['icon'],
brand: json['brand'],
);
}

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import '../../config/themes.dart';
import '../../widgets/mxc_image.dart';
import 'homeserver_app_bar.dart';
import 'homeserver_picker.dart';
@ -16,15 +16,19 @@ class HomeserverPickerView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final benchmarkResults = controller.benchmarkResults;
final identityProviders = controller.identityProviders;
final errorText = controller.error;
return LoginScaffold(
appBar: AppBar(
titleSpacing: 12,
title: Padding(
padding: const EdgeInsets.all(0.0),
child: HomeserverAppBar(controller: controller),
),
),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: HomeserverAppBar(controller: controller),
),
// display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are
// usually forced to logout as TOR browser is non-persistent
@ -49,108 +53,119 @@ class HomeserverPickerView extends StatelessWidget {
),
),
Expanded(
child: controller.displayServerList
? ListView(
child: controller.isLoading
? const Center(child: CircularProgressIndicator.adaptive())
: ListView(
children: [
if (controller.displayServerList)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
color: Theme.of(context)
.colorScheme
.onInverseSurface,
clipBehavior: Clip.hardEdge,
child: benchmarkResults == null
? const Center(
child: Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator
.adaptive(),
),
)
: Column(
children: controller.filteredHomeservers
.map(
(server) => ListTile(
trailing: IconButton(
icon: const Icon(
Icons.info_outlined,
color: Colors.black,
),
onPressed: () => controller
.showServerInfo(server),
),
onTap: () => controller.setServer(
server.homeserver.baseUrl.host,
),
title: Text(
server.homeserver.baseUrl.host,
style: const TextStyle(
color: Colors.black,
),
),
subtitle: Text(
server.homeserver.description ??
'',
style: TextStyle(
color: Colors.grey.shade700,
),
),
),
)
.toList(),
),
Image.asset(
'assets/info-logo.png',
height: 96,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 1,
height: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.continueWith,
style: const TextStyle(fontSize: 12),
),
),
Expanded(
child: Divider(
thickness: 1,
height: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
if (errorText != null) ...[
const Center(
child: Icon(
Icons.error_outline,
size: 48,
color: Colors.orange,
),
),
],
)
: Container(
alignment: Alignment.topCenter,
child: Image.asset(
'assets/banner_transparent.png',
filterQuality: FilterQuality.medium,
),
),
),
SafeArea(
child: Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextButton(
onPressed: () => launchUrlString(AppConfig.privacyUrl),
child: Text(L10n.of(context)!.privacy),
),
TextButton(
onPressed: controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
),
Hero(
tag: 'loginButton',
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primary,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
const SizedBox(height: 12),
Center(
child: Text(
errorText,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 18,
),
),
),
Center(
child: Text(
L10n.of(context)!
.pleaseTryAgainLaterOrChooseDifferentServer,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
const SizedBox(height: 12),
],
if (identityProviders != null) ...[
...identityProviders.map(
(provider) => _LoginButton(
icon: provider.icon == null
? const Icon(Icons.open_in_new_outlined)
: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: MxcImage(
placeholder: (_) =>
const Icon(Icons.web_outlined),
uri: Uri.parse(provider.icon!),
width: 24,
height: 24,
),
),
label: provider.name ??
provider.brand ??
L10n.of(context)!.singlesignon,
onPressed: () =>
controller.ssoLoginAction(provider.id!),
),
),
],
if (controller.supportsPasswordLogin)
_LoginButton(
onPressed: controller.login,
icon: const Icon(Icons.login_outlined),
label: L10n.of(context)!.signInWithPassword,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
),
),
onPressed: controller.isLoading
? null
: controller.checkHomeserverAction,
icon: const Icon(Icons.start_outlined),
label: controller.isLoading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.letsStart),
),
],
),
],
),
),
),
],
),
@ -158,3 +173,33 @@ class HomeserverPickerView extends StatelessWidget {
);
}
}
class _LoginButton extends StatelessWidget {
final Widget icon;
final String label;
final void Function() onPressed;
const _LoginButton({
required this.icon,
required this.label,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
margin: const EdgeInsets.only(bottom: 16),
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: icon,
label: Text(label),
),
);
}
}

View File

@ -109,6 +109,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
Widget body;
final buttons = <Widget>[];
switch (widget.request.state) {
case KeyVerificationState.showQRSuccess:
case KeyVerificationState.confirmQRScan:
case KeyVerificationState.askChoice:
throw 'Not implemented';
case KeyVerificationState.askSSSS:
// prompt the user for their ssss passphrase / key
final textEditingController = TextEditingController();

View File

@ -59,7 +59,7 @@ class NewPrivateChatView extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrImage(
QrImageView(
data:
'https://matrix.to/#/${Matrix.of(context).client.userID}',
version: QrVersions.auto,

View File

@ -151,12 +151,11 @@ class EmotesSettingsController extends State<EmotesSettings> {
bool isGloballyActive(Client? client) =>
room != null &&
client!.accountData['im.ponies.emote_rooms']?.content is Map &&
client.accountData['im.ponies.emote_rooms']!.content['rooms'] is Map &&
client.accountData['im.ponies.emote_rooms']!.content['rooms'][room!.id]
is Map &&
client.accountData['im.ponies.emote_rooms']!.content['rooms'][room!.id]
[stateKey ?? ''] is Map;
client!.accountData['im.ponies.emote_rooms']?.content
.tryGetMap<String, Object?>('rooms')
?.tryGetMap<String, Object?>(room!.id)
?.tryGetMap<String, Object?>(stateKey ?? '') !=
null;
bool get readonly =>
room == null ? false : !(room!.canSendEvent('im.ponies.room_emotes'));

View File

@ -40,16 +40,14 @@ class MultipleEmotesSettingsView extends StatelessWidget {
itemCount: keys.length,
itemBuilder: (BuildContext context, int i) {
final event = packs[keys[i]];
String? packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
if (event != null && event.content['pack'] is Map) {
if (event.content['pack']['displayname'] is String) {
packName = event.content['pack']['displayname'];
} else if (event.content['pack']['name'] is String) {
packName = event.content['pack']['name'];
}
}
final eventPack =
event?.content.tryGetMap<String, Object?>('pack');
final packName = eventPack?.tryGet<String>('displayname') ??
eventPack?.tryGet<String>('name') ??
(keys[i].isNotEmpty ? keys[i] : 'Default Pack');
return ListTile(
title: Text(packName!),
title: Text(packName),
onTap: () async {
VRouter.of(context).toSegments(
['rooms', room.id, 'details', 'emotes', keys[i]],

View File

@ -1,129 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/sign_up/signup_view.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/localized_exception_extension.dart';
class SignupPage extends StatefulWidget {
const SignupPage({Key? key}) : super(key: key);
@override
SignupPageController createState() => SignupPageController();
}
class SignupPageController extends State<SignupPage> {
final TextEditingController passwordController = TextEditingController();
final TextEditingController password2Controller = TextEditingController();
final TextEditingController emailController = TextEditingController();
String? error;
bool loading = false;
bool showPassword = false;
bool noEmailWarningConfirmed = false;
bool displaySecondPasswordField = false;
static const int minPassLength = 8;
void toggleShowPassword() => setState(() => showPassword = !showPassword);
String? get domain => VRouter.of(context).queryParameters['domain'];
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
void onPasswordType(String text) {
if (text.length >= minPassLength && !displaySecondPasswordField) {
setState(() {
displaySecondPasswordField = true;
});
}
}
String? password1TextFieldValidator(String? value) {
if (value!.isEmpty) {
return L10n.of(context)!.chooseAStrongPassword;
}
if (value.length < minPassLength) {
return L10n.of(context)!
.pleaseChooseAtLeastChars(minPassLength.toString());
}
return null;
}
String? password2TextFieldValidator(String? value) {
if (value!.isEmpty) {
return L10n.of(context)!.repeatPassword;
}
if (value != passwordController.text) {
return L10n.of(context)!.passwordsDoNotMatch;
}
return null;
}
String? emailTextFieldValidator(String? value) {
if (value!.isEmpty && !noEmailWarningConfirmed) {
noEmailWarningConfirmed = true;
return L10n.of(context)!.noEmailWarning;
}
if (value.isNotEmpty && !value.contains('@')) {
return L10n.of(context)!.pleaseEnterValidEmail;
}
return null;
}
void signup([_]) async {
setState(() {
error = null;
});
if (!formKey.currentState!.validate()) return;
setState(() {
loading = true;
});
try {
final client = Matrix.of(context).getLoginClient();
final email = emailController.text;
if (email.isNotEmpty) {
Matrix.of(context).currentClientSecret =
DateTime.now().millisecondsSinceEpoch.toString();
Matrix.of(context).currentThreepidCreds =
await client.requestTokenToRegisterEmail(
Matrix.of(context).currentClientSecret,
email,
0,
);
}
final displayname = Matrix.of(context).loginUsername!;
final localPart = displayname.toLowerCase().replaceAll(' ', '_');
await client.uiaRequestBackground(
(auth) => client.register(
username: localPart,
password: passwordController.text,
initialDeviceDisplayName: PlatformInfos.clientName,
auth: auth,
),
);
// Set displayname
if (displayname != localPart) {
await client.setDisplayName(
client.userID!,
displayname,
);
}
} catch (e) {
error = (e).toLocalizedString(context);
} finally {
if (mounted) {
setState(() => loading = false);
}
}
}
@override
Widget build(BuildContext context) => SignupPageView(this);
}

View File

@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import 'signup.dart';
class SignupPageView extends StatelessWidget {
final SignupPageController controller;
const SignupPageView(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return LoginScaffold(
appBar: AppBar(
leading: controller.loading ? null : const BackButton(),
automaticallyImplyLeading: !controller.loading,
title: Text(L10n.of(context)!.signUp),
),
body: Form(
key: controller.formKey,
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextFormField(
readOnly: controller.loading,
autocorrect: false,
onChanged: controller.onPasswordType,
autofillHints:
controller.loading ? null : [AutofillHints.newPassword],
controller: controller.passwordController,
obscureText: !controller.showPassword,
validator: controller.password1TextFieldValidator,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.vpn_key_outlined),
suffixIcon: IconButton(
tooltip: L10n.of(context)!.showPassword,
icon: Icon(
controller.showPassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black,
),
onPressed: controller.toggleShowPassword,
),
errorStyle: const TextStyle(color: Colors.orange),
hintText: L10n.of(context)!.chooseAStrongPassword,
),
),
),
if (controller.displaySecondPasswordField)
Padding(
padding: const EdgeInsets.all(12.0),
child: TextFormField(
readOnly: controller.loading,
autocorrect: false,
autofillHints:
controller.loading ? null : [AutofillHints.newPassword],
controller: controller.password2Controller,
obscureText: !controller.showPassword,
validator: controller.password2TextFieldValidator,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.repeat_outlined),
hintText: L10n.of(context)!.repeatPassword,
errorStyle: const TextStyle(color: Colors.orange),
),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: TextFormField(
readOnly: controller.loading,
autocorrect: false,
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
autofillHints:
controller.loading ? null : [AutofillHints.username],
validator: controller.emailTextFieldValidator,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.mail_outlined),
hintText: L10n.of(context)!.enterAnEmailAddress,
errorText: controller.error,
errorMaxLines: 4,
errorStyle: TextStyle(
color: controller.emailController.text.isEmpty
? Colors.orangeAccent
: Colors.orange,
),
),
),
),
Hero(
tag: 'loginButton',
child: Padding(
padding: const EdgeInsets.all(12),
child: ElevatedButton.icon(
icon: const Icon(Icons.person_add_outlined),
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed: controller.loading ? () {} : controller.signup,
label: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.signUp),
),
),
),
],
),
),
);
}
}

View File

@ -182,8 +182,12 @@ class MatrixLocals extends MatrixLocalizations {
String get noPermission => l10n.noKeyForThisMessage;
@override
String redactedAnEvent(String senderName) {
return l10n.redactedAnEvent(senderName);
String redactedAnEvent(Event redactedEvent) {
return l10n.redactedAnEvent(
redactedEvent.redactedBecause?.senderFromMemoryOrFallback
.calcLocalizedBodyFallback(this) ??
l10n.user,
);
}
@override
@ -192,8 +196,10 @@ class MatrixLocals extends MatrixLocalizations {
}
@override
String removedBy(String calcDisplayname) {
return l10n.removedBy(calcDisplayname);
String removedBy(Event redactedEvent) {
return l10n.removedBy(
redactedEvent.senderFromMemoryOrFallback.calcLocalizedBodyFallback(this),
);
}
@override

View File

@ -198,11 +198,33 @@ Future<void> _tryPushHelper(
final roomName = event.room.getLocalizedDisplayname(MatrixLocals(l10n));
final notificationGroupId =
event.room.isDirectChat ? 'directChats' : 'groupChats';
final groupName = event.room.isDirectChat ? l10n.directChats : l10n.groups;
final messageRooms = AndroidNotificationChannelGroup(
notificationGroupId,
groupName,
);
final roomsChannel = AndroidNotificationChannel(
event.room.id,
roomName,
groupId: notificationGroupId,
);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannelGroup(messageRooms);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(roomsChannel);
final androidPlatformChannelSpecifics = AndroidNotificationDetails(
event.room.id,
roomName,
channelDescription:
event.room.isDirectChat ? l10n.directChats : l10n.groups,
channelDescription: groupName,
number: notification.counts?.unread,
category: AndroidNotificationCategory.message,
styleInformation: messagingStyleInformation ??
@ -215,7 +237,7 @@ Future<void> _tryPushHelper(
ticker: l10n.unreadChats(notification.counts?.unread ?? 1),
importance: Importance.max,
priority: Priority.max,
groupKey: event.room.id,
groupKey: notificationGroupId,
);
const iOSPlatformChannelSpecifics = DarwinNotificationDetails();
final platformChannelSpecifics = NotificationDetails(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart' show IterableExtension;
@ -37,14 +38,34 @@ class UrlLauncher {
);
return;
}
final consent = await showOkCancelAlertDialog(
final consent = await showModalActionSheet<_LaunchUrlResponse>(
context: context,
title: L10n.of(context)!.openLinkInBrowser,
message: url,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
title: url,
style: AdaptiveStyle.material,
actions: [
SheetAction(
key: _LaunchUrlResponse.copy,
icon: Icons.copy_outlined,
label: L10n.of(context)!.copy,
),
SheetAction(
key: _LaunchUrlResponse.launch,
icon: Icons.launch_outlined,
label: L10n.of(context)!.openLinkInBrowser,
),
],
);
if (consent != OkCancelResult.ok) return;
if (consent == _LaunchUrlResponse.copy) {
await Clipboard.setData(ClipboardData(text: uri.toString()));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.copiedToClipboard),
),
);
return;
}
if (consent != _LaunchUrlResponse.launch) return;
if (!{'https', 'http'}.contains(uri.scheme)) {
// just launch non-https / non-http uris directly
@ -215,3 +236,8 @@ class UrlLauncher {
}
}
}
enum _LaunchUrlResponse {
launch,
copy,
}

View File

@ -1,7 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/platform_infos.dart';
class LoginScaffold extends StatelessWidget {
final Widget body;
@ -17,6 +21,7 @@ class LoginScaffold extends StatelessWidget {
Widget build(BuildContext context) {
final isMobileMode = !FluffyThemes.isColumnMode(context);
final scaffold = Scaffold(
key: const Key('LoginScaffold'),
backgroundColor: isMobileMode ? null : Colors.transparent,
appBar: appBar == null
? null
@ -33,31 +38,93 @@ class LoginScaffold extends StatelessWidget {
extendBodyBehindAppBar: true,
extendBody: true,
body: body,
bottomNavigationBar: isMobileMode
? Material(
color: Theme.of(context).colorScheme.onInverseSurface,
child: const _PrivacyButtons(
mainAxisAlignment: MainAxisAlignment.center,
),
)
: null,
);
if (isMobileMode) return scaffold;
return Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/login_wallpaper.png'),
fit: BoxFit.cover,
),
decoration: BoxDecoration(
gradient: FluffyThemes.backgroundGradient(context, 156),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.925),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
elevation: 10,
shadowColor: Colors.black,
child: ConstrainedBox(
constraints: isMobileMode
? const BoxConstraints()
: const BoxConstraints(maxWidth: 480, maxHeight: 640),
child: scaffold,
child: Column(
children: [
const SizedBox(height: 64),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Material(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
child: ConstrainedBox(
constraints: isMobileMode
? const BoxConstraints()
: const BoxConstraints(maxWidth: 960, maxHeight: 640),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Image.asset(
'assets/login_wallpaper.png',
fit: BoxFit.cover,
),
),
Container(
width: 1,
color: Theme.of(context).dividerTheme.color,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: scaffold,
),
),
],
),
),
),
),
),
),
const _PrivacyButtons(mainAxisAlignment: MainAxisAlignment.end),
],
),
);
}
}
class _PrivacyButtons extends StatelessWidget {
final MainAxisAlignment mainAxisAlignment;
const _PrivacyButtons({required this.mainAxisAlignment});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 64,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: mainAxisAlignment,
children: [
TextButton(
onPressed: () => PlatformInfos.showDialog(context),
child: Text(L10n.of(context)!.about),
),
TextButton(
onPressed: () => launchUrlString(AppConfig.privacyUrl),
child: Text(L10n.of(context)!.privacy),
),
],
),
),
);

View File

@ -10,7 +10,9 @@
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
#include <dynamic_color/dynamic_color_plugin.h>
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <handy_window/handy_window_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@ -29,9 +31,15 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin");
emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
g_autoptr(FlPluginRegistrar) handy_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin");
handy_window_plugin_register_with_registrar(handy_window_registrar);

View File

@ -7,7 +7,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_lifecycle
dynamic_color
emoji_picker_flutter
file_selector_linux
flutter_secure_storage_linux
flutter_webrtc
handy_window
record_linux
url_launcher_linux

View File

@ -12,6 +12,7 @@ import desktop_lifecycle
import device_info_plus
import dynamic_color
import emoji_picker_flutter
import file_selector_macos
import flutter_app_badger
import flutter_local_notifications
import flutter_secure_storage_macos
@ -19,15 +20,18 @@ import flutter_web_auth_2
import flutter_webrtc
import geolocator_apple
import just_audio
import macos_ui
import macos_window_utils
import package_info_plus
import path_provider_foundation
import record_macos
import share_plus
import shared_preferences_macos
import shared_preferences_foundation
import sqflite
import url_launcher_macos
import video_compress
import wakelock_macos
import wakelock_plus
import window_to_front
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@ -38,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
@ -45,6 +50,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin"))
@ -54,5 +61,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
}

File diff suppressed because it is too large Load Diff

View File

@ -7,19 +7,19 @@ environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
adaptive_dialog: ^1.9.0-no-macos.2
adaptive_dialog: ^1.9.0-x-macos-beta.1
animations: ^2.0.7
badges: ^2.0.3
blurhash_dart: ^1.1.0
callkeep: ^0.3.2
chewie: ^1.3.6
collection: ^1.16.0
connectivity_plus: ^3.0.2
connectivity_plus: ^4.0.1
cupertino_icons: any
desktop_drop: ^0.4.0
desktop_lifecycle: ^0.1.0
desktop_notifications: ^0.6.3
device_info_plus: ^8.0.0
device_info_plus: ^9.0.2
dynamic_color: ^1.6.0
emoji_picker_flutter: ^1.5.1
emoji_proposal: ^0.0.1
@ -32,37 +32,37 @@ dependencies:
flutter_app_lock: ^3.0.0
flutter_blurhash: ^0.7.0
flutter_cache_manager: ^3.3.0
flutter_foreground_task: ^3.10.0
flutter_foreground_task: ^6.0.0+1
flutter_highlighter: ^0.1.1
flutter_html: ^3.0.0-beta.2
flutter_html_table: ^3.0.0-beta.2
flutter_linkify: ^6.0.0
flutter_local_notifications: ^12.0.2
flutter_local_notifications: ^15.1.0+1
flutter_localizations:
sdk: flutter
flutter_map: ^3.1.0
flutter_map: ^4.0.0
flutter_math_fork: ^0.7.1
flutter_olm: ^1.2.0
flutter_openssl_crypto: ^0.1.0
flutter_ringtone_player: ^3.1.1
flutter_secure_storage: ^7.0.1
flutter_secure_storage: ^8.0.0
flutter_typeahead: ^4.3.2
flutter_web_auth_2: ^2.1.1
flutter_webrtc: ^0.9.30+hotfix.2
flutter_webrtc: ^0.9.35
future_loading_dialog: ^0.2.3
geolocator: ^7.6.2
handy_window: ^0.1.9
handy_window: ^0.3.1
hive: ^2.2.3
hive_flutter: ^1.1.0
http: ^0.13.4
image_picker: ^0.8.4+8
image_picker: ^1.0.0
intl: any
just_audio: ^0.9.30
just_audio_mpv: ^0.1.6
keyboard_shortcuts: ^0.1.4
latlong2: ^0.8.1
linkify: ^5.0.0
matrix: ^0.20.5
matrix: ^0.22.0
matrix_homeserver_recommendations: ^0.3.0
native_imaging: ^0.1.0
package_info_plus: ^4.0.0
@ -77,12 +77,12 @@ dependencies:
record: ^4.4.4
scroll_to_index: ^3.0.1
share_plus: ^7.0.0
shared_preferences: 2.0.15 # Pinned because https://github.com/flutter/flutter/issues/118401
shared_preferences: ^2.2.0 # Pinned because https://github.com/flutter/flutter/issues/118401
slugify: ^2.0.0
swipe_to_action: ^0.2.0
tor_detector_web: ^1.1.0
uni_links: ^0.5.1
unifiedpush: ^4.0.3
unifiedpush: ^5.0.0
universal_html: ^2.0.8
url_launcher: ^6.0.20
vibration: ^1.7.4-nullsafety.0
@ -93,7 +93,7 @@ dependencies:
webrtc_interface: ^1.0.13
dev_dependencies:
dart_code_metrics: ^4.10.1
dart_code_metrics: ^5.7.5
flutter_lints: ^2.0.1
flutter_native_splash: ^2.0.3+1
flutter_test:
@ -153,8 +153,6 @@ dependency_overrides:
git:
url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git
ref: main
flutter_webrtc:
git: https://github.com/krille-chan/flutter-webrtc.git
geolocator_android:
hosted:
name: geolocator_android
@ -169,6 +167,6 @@ dependency_overrides:
# https://github.com/creativecreatorormaybenot/wakelock/pull/203
wakelock_windows:
git:
url: https://github.com/timsneath/wakelock.git
ref: 2a9bca63a540771f241d688562351482b2cf234c
path: wakelock_windows
url: https://github.com/chandrabezzo/wakelock.git
ref: main
path: wakelock_windows/

View File

@ -11,6 +11,7 @@
#include <desktop_lifecycle/desktop_lifecycle_plugin.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_lifecycle
dynamic_color
emoji_picker_flutter
file_selector_windows
flutter_webrtc
permission_handler_windows
record_windows