Merge pull request #3366 from pangeachat/fluffychat-merge

Fluffychat merge
pull/2245/head
ggurdin 4 months ago committed by GitHub
commit 0ce8758f64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -20,9 +20,12 @@ jobs:
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: flutter pub get
- uses: moonrepo/setup-rust@v1
- run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
- name: Prepare web
run: ./scripts/prepare-web.sh
- run: rm ./assets/vodozemac/.gitignore
- run: flutter pub get
- name: Build Release Web
run: ./scripts/build-web.sh

@ -48,6 +48,8 @@ jobs:
cache: true
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install nodejs -y
- uses: moonrepo/setup-rust@v1
- run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
- run: flutter pub get
- name: Prepare web
run: ./scripts/prepare-web.sh
@ -201,4 +203,4 @@ jobs:
aws s3 sync ./build/web s3://$WEBAPP_S3_BUCKET
- name: AWS CloudFront Invalidation
run: |
aws cloudfront create-invalidation --distribution-id $CF_DISTRIBUTION_ID --paths "/*"
aws cloudfront create-invalidation --distribution-id $CF_DISTRIBUTION_ID --paths "/*"

@ -1,2 +1,2 @@
FLUTTER_VERSION=3.32.1
FLUTTER_VERSION=3.32.4
JAVA_VERSION=17

1546
.gitignore vendored

File diff suppressed because it is too large Load Diff

@ -1,3 +1,24 @@
## v2.0.0
This version migrates to Vodozemac and Matrix Dart SDK 1.0.0. This is a breaking
change. The user should not notice the migration at all but downgrading from
v2.0.0 to a previous version is not possible without losing the session.
- fix: Do not set read markers for sending events (Christian Kußowski)
- fix: fix compile error related to MxcImage (gilice)
- fix: Forward last version of events when forwarding (Christian Kußowski)
- fix: Ban button displayed for already banned users (Christian Kußowski)
- fix: Route back to room list after leaving a chat (Christian Kußowski)
- build: Switch to matrix sdk 1.0.0 (Christian Kußowski)
- build: Upgrade flutter to 3.32.2 (krille-chan)
- build: Update to flutter 3.32.4 (Christian Kußowski)
- chore: Add missing mounted check (Christian Kußowski)
- chore: highlight select mode actions (Christian Kußowski)
- refactor: sdk 1.0 (Christian Kußowski)
- refactor: New message context menu (Christian Kußowski)
- refactor: Nicer popupmenus (Christian Kußowski)
- Translated using Weblate (Spanish) (Kimby)
## v1.27.0
- feat: Add confirmation dialog before accepting invite (krille-chan)
- feat: Add feature flag for refresh tokens (Christian Kußowski)

@ -1,5 +1,5 @@
FROM ghcr.io/cirruslabs/flutter as builder
RUN sudo apt update && sudo apt install curl wget jq -y
RUN sudo apt update && sudo apt install curl wget jq build-essential -y
WORKDIR /tmp
RUN wget https://github.com/mikefarah/yq/releases/download/v4.40.5/yq_linux_amd64.tar.gz
@ -8,6 +8,9 @@ RUN mv yq_linux_amd64 /usr/bin/yq
COPY . /app
WORKDIR /app
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
RUN ./scripts/prepare-web.sh
COPY config.* /app/
RUN flutter pub get

@ -42,7 +42,7 @@
>
<activity
android:name=".MainActivity"
android:launchMode="singleInstance"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"

@ -0,0 +1 @@
vodozemac_bindings_dart*

@ -46,7 +46,12 @@
target="_blank" class="text-xl underline hover:text-purple-800 dark:hover:text-purple-400">matrix</a>]
</p>
<img src="screenshots/screenshots.png" alt="Mobile and desktop screenshots" class="max-w-xl mb-16 w-full px-8" />
<div class="flex flex-wrap justify-center mb-16 w-full px-8 gap-4">
<img src="screenshots/mobile.png" alt="Mobile screenshot"
class="h-96 w-auto object-contain rounded-xl border border-gray-300 shadow-xl" />
<img src="screenshots/desktop.png" alt="Desktop screenshot"
class="h-96 w-auto object-contain rounded-xl border border-gray-300 shadow-xl" />
</div>
<div class="max-w-lg mb-16 flex justify-center flex-wrap">
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

@ -1,3 +1,8 @@
source "https://rubygems.org"
gem "fastlane"
# Workaround for ruby 3.4 https://github.com/fastlane/fastlane/issues/29183
gem "abbrev"
gem "mutex_m"
gem "ostruct"

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/archive/archive.dart';
@ -60,7 +61,9 @@ abstract class AppRoutes {
GoRouterState state,
) {
// #Pangea
// Matrix.of(context).client.isLogged() ? '/rooms' : null;
// Matrix.of(context).widget.clients.any((client) => client.isLogged())
// ? '/rooms'
// : null;
return PAuthGaurd.loggedInRedirect(context, state);
// Pangea#
}
@ -70,7 +73,9 @@ abstract class AppRoutes {
GoRouterState state,
) {
// #Pangea
// Matrix.of(context).client.isLogged() ? null : '/home';
// Matrix.of(context).widget.clients.any((client) => client.isLogged())
// ? null
// : '/home';
return PAuthGaurd.loggedOutRedirect(context, state);
// Pangea#
}
@ -81,7 +86,9 @@ abstract class AppRoutes {
GoRoute(
path: '/',
redirect: (context, state) =>
Matrix.of(context).client.isLogged() ? '/rooms' : '/home',
Matrix.of(context).widget.clients.any((client) => client.isLogged())
? '/rooms'
: '/home',
),
GoRoute(
path: '/home',
@ -100,7 +107,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const Login(),
Login(client: state.extra as Client),
),
redirect: loggedInRedirect,
),
@ -430,7 +437,7 @@ abstract class AppRoutes {
// pageBuilder: (context, state) => defaultPageBuilder(
// context,
// state,
// const Login(),
// Login(client: state.extra as Client),
// ),
// redirect: loggedOutRedirect,
// ),
@ -448,17 +455,6 @@ abstract class AppRoutes {
},
redirect: loggedOutRedirect,
),
GoRoute(
path: 'homeserver',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
const SettingsHomeserver(),
);
},
redirect: loggedOutRedirect,
),
GoRoute(
path: 'security',
redirect: loggedOutRedirect,

@ -7,6 +7,8 @@ import 'app_config.dart';
abstract class FluffyThemes {
static const double columnWidth = 380.0;
static const double maxTimelineWidth = columnWidth * 2;
// #Pangea
// static const double navRailWidth = 80.0;
static const double navRailWidth = 72.0;
@ -63,8 +65,11 @@ abstract class FluffyThemes {
? colorScheme.surfaceContainerHighest
: colorScheme.surfaceContainer,
popupMenuTheme: PopupMenuThemeData(
color: colorScheme.surfaceContainerLow,
iconColor: colorScheme.onSurface,
textStyle: TextStyle(color: colorScheme.onSurface),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
),
segmentedButtonTheme: SegmentedButtonThemeData(

@ -1,29 +1,35 @@
{
"repeatPassword": "Gentag password",
"@repeatPassword": {},
"notAnImage": "Ikke en billedfil.",
"@notAnImage": {},
"setCustomPermissionLevel": "Indstil særligt tilladelsesniveau",
"@setCustomPermissionLevel": {},
"setPermissionsLevelDescription": "Vælg en prædefineret rolle herunder eller indtaste et særligt tilladelsesniveau mellem 0 og 100.",
"@setPermissionsLevelDescription": {},
"ignoreUser": "Ignorér bruger",
"@ignoreUser": {},
"remove": "Fjern",
"@remove": {
"type": "String",
"placeholders": {}
},
"importNow": "Importer nu",
"@importNow": {},
"importEmojis": "Importer emojis",
"@importEmojis": {},
"normalUser": "Normal bruger",
"@normalUser": {},
"importFromZipFile": "Importer fra .zip fil",
"@importFromZipFile": {},
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
}
"repeatPassword": "Gentag password",
"@repeatPassword": {},
"notAnImage": "Ikke en billedfil.",
"@notAnImage": {},
"setCustomPermissionLevel": "Indstil særligt tilladelsesniveau",
"@setCustomPermissionLevel": {},
"setPermissionsLevelDescription": "Vælg en prædefineret rolle herunder eller indtaste et særligt tilladelsesniveau mellem 0 og 100.",
"@setPermissionsLevelDescription": {},
"ignoreUser": "Ignorér bruger",
"@ignoreUser": {},
"remove": "Fjern",
"@remove": {
"type": "String",
"placeholders": {}
},
"importNow": "Importer nu",
"@importNow": {},
"importEmojis": "Importer emojis",
"@importEmojis": {},
"normalUser": "Normal bruger",
"@normalUser": {},
"importFromZipFile": "Importer fra .zip fil",
"@importFromZipFile": {},
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"exportEmotePack": "Eksportér Emote-pakke som .zip-fil",
"@exportEmotePack": {},
"replace": "Erstat",
"@replace": {},
"about": "Om",
"@about": {}
}

@ -3240,6 +3240,7 @@
"commandHint_logout": "Logout your current device",
"commandHint_logoutall": "Logout all active devices",
"displayNavigationRail": "Show navigation rail on mobile",
"customReaction": "Custom reaction",
"accountInformation": "Account information",
"addGroupDescription": "Add a chat description",
"addNewFriend": "Add new friend",

@ -3342,6 +3342,51 @@
"@notSupportedOnThisDevice": {},
"enterNewChat": "Ingresar a nuevo chat",
"@enterNewChat": {},
"pleaseWaitUntilInvited": "Por favor espera, hasta que alguien del chat te invite.",
"@pleaseWaitUntilInvited": {},
"commandHint_roomupgrade": "Actualizar este chat a la versión de chat dada",
"@commandHint_roomupgrade": {},
"checkList": "Lista de tareas",
"@checkList": {},
"countInvited": "{count} invitado",
"@countInvited": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"sentVoiceMessage": "🎙️ {duration} - Mensaje de voz de {sender}",
"@sentVoiceMessage": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
},
"duration": {
"type": "String"
}
}
},
"setCustomPermissionLevel": "Agregar nivel personalizado de permiso",
"@setCustomPermissionLevel": {},
"setPermissionsLevelDescription": "Por favor elige un rol predeterminado o un nivel de permiso personalizado entre 0 a 100.",
"@setPermissionsLevelDescription": {},
"ignoreUser": "Ignorar usuario",
"@ignoreUser": {},
"normalUser": "Usuario normal",
"@normalUser": {},
"commandHint_logout": "Salir del dispositivo actual",
"@commandHint_logout": {},
"commandHint_logoutall": "Salir de todos los dispositivos activos",
"@commandHint_logoutall": {},
"displayNavigationRail": "Mostrar carril de navegación en móvil",
"@displayNavigationRail": {},
"youHaveKnocked": "Has sido golpeado",
"@youHaveKnocked": {},
"approve": "Aprobar",
"@approve": {},
"accountInformation": "Información de la cuenta",
"addGroupDescription": "Agregar una descripción al grupo",
"alreadyHaveAnAccount": "¿Ya tiene una cuenta?",
@ -5450,11 +5495,6 @@
},
"downloadGboard": "Descargar Gboard",
"autocorrectNotAvailable": "Desafortunadamente, tu plataforma no es compatible actualmente con esta función. ¡Mantente atento a futuros desarrollos!",
"setCustomPermissionLevel": "Establecer nivel de permiso personalizado",
"setPermissionsLevelDescription": "Por favor, elige un rol predefinido a continuación o ingresa un nivel de permiso personalizado entre 0 y 100.",
"ignoreUser": "Ignorar usuario",
"normalUser": "Usuario normal",
"commandHint_roomupgrade": "Mejorar esta sala a la versión de sala dada",
"completeActivitiesToUnlock": "¡Completa al menos una actividad para desbloquear la traducción!",
"constructUseGaDesc": "Asistencia gramatical",
"constructUseTaDesc": "Asistencia de traducción",
@ -5530,9 +5570,6 @@
"ban": "Prohibir",
"unban": "Desbloquear",
"kick": "Expulsar",
"approve": "Aprobar",
"youHaveKnocked": "Has llamado",
"pleaseWaitUntilInvited": "Por favor, espera hasta que alguien de la sala te invite.",
"lemma": "Lema",
"grammarFeature": "Característica gramatical",
"grammarTag": "Etiqueta gramatical",
@ -5560,7 +5597,6 @@
"configureSpace": "Configurar espacio",
"pinMessages": "Fijar mensajes",
"setJoinRules": "Establecer reglas de unión",
"displayNavigationRail": "Mostrar barra de navegación en móvil",
"changeGeneralSettings": "Cambiar configuraciones generales",
"inviteOtherUsersToRoom": "Invitar a otros usuarios",
"changeTheNameOfTheSpace": "Cambiar el nombre del espacio",
@ -5568,26 +5604,6 @@
"changeThePermissions": "Cambiar los permisos",
"introductions": "Introducciones",
"announcements": "Anuncios",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
},
"@setPermissionsLevelDescription": {
"type": "String",
"placeholders": {}
},
"@ignoreUser": {
"type": "String",
"placeholders": {}
},
"@normalUser": {
"type": "String",
"placeholders": {}
},
"@commandHint_roomupgrade": {
"type": "String",
"placeholders": {}
},
"@completeActivitiesToUnlock": {
"type": "String",
"placeholders": {}
@ -5908,18 +5924,6 @@
"type": "String",
"placeholders": {}
},
"@approve": {
"type": "String",
"placeholders": {}
},
"@youHaveKnocked": {
"type": "String",
"placeholders": {}
},
"@pleaseWaitUntilInvited": {
"type": "String",
"placeholders": {}
},
"@lemma": {
"type": "String",
"placeholders": {}
@ -6036,10 +6040,6 @@
"type": "String",
"placeholders": {}
},
"@displayNavigationRail": {
"type": "String",
"placeholders": {}
},
"@changeGeneralSettings": {
"type": "String",
"placeholders": {}

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -3351,7 +3351,7 @@
"@approve": {},
"youHaveKnocked": "Ви постукали",
"@youHaveKnocked": {},
"sentVoiceMessage": "🎙️ {duration} - {sender}",
"sentVoiceMessage": "🎙️ {duration} - {sender} - Голосове повідомлення від {sender}",
"@sentVoiceMessage": {
"type": "String",
"placeholders": {
@ -3373,5 +3373,9 @@
}
},
"checkList": "Контрольний список",
"@checkList": {}
"@checkList": {},
"commandHint_logout": "Вийти на цьому пристрої",
"@commandHint_logout": {},
"commandHint_logoutall": "Вийти на всіх активних пристроях",
"@commandHint_logoutall": {}
}

File diff suppressed because it is too large Load Diff

@ -50,6 +50,10 @@ void main() async {
// widget bindings are initialized already.
WidgetsFlutterBinding.ensureInitialized();
// #Pangea
// await vod.init(wasmPath: './assets/assets/vodozemac/');
// Pangea#
Logs().nativeColors = !PlatformInfos.isIOS;
final store = await SharedPreferences.getInstance();
final clients = await ClientManager.getClients(store: store);

@ -1,9 +1,6 @@
// ignore_for_file: depend_on_referenced_packages, implementation_imports
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -252,8 +249,6 @@ class ChatController extends State<ChatPageWithRoom>
context.go('/rooms');
}
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
// #Pangea
// void requestHistory([_]) async {
Future<void> requestHistory() async {
@ -680,6 +675,7 @@ class ChatController extends State<ChatPageWithRoom>
}
// Pangea#
if (state != AppLifecycleState.resumed) return;
if (!mounted) return;
setReadMarker();
}
@ -695,6 +691,7 @@ class ChatController extends State<ChatPageWithRoom>
return;
}
// Pangea#
if (eventId?.isValidMatrixId == false) return;
if (_setReadMarkerFuture != null) return;
if (_scrolledUp) return;
if (scrollUpBannerEventId != null) return;
@ -933,7 +930,6 @@ class ChatController extends State<ChatPageWithRoom>
// editEventId: editEvent?.eventId,
// parseCommands: parseCommands,
// );
// If the message and the sendController text don't match, it's possible
// that there was a delay in tokenization before send, and the user started
// typing a new message. We don't want to erase that, so only reset the input
@ -1040,7 +1036,6 @@ class ChatController extends State<ChatPageWithRoom>
// text: pendingText,
// selection: const TextSelection.collapsed(offset: 0),
// );
// Pangea#
setState(() {
// #Pangea
@ -1235,13 +1230,11 @@ class ChatController extends State<ChatPageWithRoom>
} else {
inputFocus.unfocus();
}
emojiPickerType = EmojiPickerType.keyboard;
setState(() => showEmojiPicker = !showEmojiPicker);
}
void _inputFocusListener() {
if (showEmojiPicker && inputFocus.hasFocus) {
emojiPickerType = EmojiPickerType.keyboard;
setState(() => showEmojiPicker = false);
}
}
@ -1443,9 +1436,6 @@ class ChatController extends State<ChatPageWithRoom>
bool get canEditSelectedEvents {
if (isArchived ||
selectedEvents.length != 1 ||
// #Pangea
selectedEvents.single.messageType != MessageTypes.Text ||
// Pangea#
!selectedEvents.first.status.isSent) {
return false;
}
@ -1455,21 +1445,18 @@ class ChatController extends State<ChatPageWithRoom>
void forwardEventsAction() async {
if (selectedEvents.isEmpty) return;
final timeline = this.timeline;
if (timeline == null) return;
final forwardEvents = List<Event>.from(selectedEvents)
.map((event) => event.getDisplayEvent(timeline))
.toList();
await showScaffoldDialog(
context: context,
builder: (context) => ShareScaffoldDialog(
items: selectedEvents
// #Pangea
// https://github.com/pangeachat/client/issues/2934
// .map((event) => ContentShareItem(event.content))
.map(
(event) => timeline != null
? ContentShareItem(
event.getDisplayEvent(timeline!).content,
)
: ContentShareItem(event.content),
)
// Pangea#
items: forwardEvents
.map((event) => ContentShareItem(event.content))
.toList(),
),
);
@ -1576,34 +1563,8 @@ class ChatController extends State<ChatPageWithRoom>
}
void onEmojiSelected(_, Emoji? emoji) {
switch (emojiPickerType) {
case EmojiPickerType.reaction:
senEmojiReaction(emoji);
break;
case EmojiPickerType.keyboard:
typeEmoji(emoji);
onInputBarChanged(sendController.text);
break;
}
}
void senEmojiReaction(Emoji? emoji) {
setState(() => showEmojiPicker = false);
if (emoji == null) return;
// make sure we don't send the same emoji twice
if (_allReactionEvents.any(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
)) {
return;
}
// #Pangea
// return sendEmojiAction(emoji.emoji);
sendEmojiAction(emoji.emoji);
// don't need to clear these when sending while in select mode,
// but do need to clear these when reacting from the large emoji picker
setState(() => selectedEvents.clear());
// Pangea#
typeEmoji(emoji);
onInputBarChanged(sendController.text);
}
void typeEmoji(Emoji? emoji) {
@ -1618,7 +1579,6 @@ class ChatController extends State<ChatPageWithRoom>
);
}
// Pangea#
final selection = sendController.selection;
final newText = sendController.text.isEmpty
? emoji.emoji
@ -1632,54 +1592,12 @@ class ChatController extends State<ChatPageWithRoom>
);
}
late Iterable<Event> _allReactionEvents;
void emojiPickerBackspace() {
switch (emojiPickerType) {
case EmojiPickerType.reaction:
setState(() => showEmojiPicker = false);
break;
case EmojiPickerType.keyboard:
sendController
..text = sendController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: sendController.text.length),
);
break;
}
}
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea
closeSelectionOverlay();
// Pangea#
_allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction;
setState(() => showEmojiPicker = true);
}
void sendEmojiAction(String? emoji) async {
final events = List<Event>.from(selectedEvents);
// #Pangea
// keep this event selected in case the user wants to send another emoji
// setState(() => selectedEvents.clear());
// Pangea#
// if reaction already exists, don't send it again
if (timeline == null ||
events.any(
(e) => e.aggregatedEvents(timeline!, RelationshipTypes.reaction).any(
(re) => re.content.tryGetMap('m.relates_to')?['key'] == emoji,
),
)) {
return;
}
for (final event in events) {
await room.sendReaction(
event.eventId,
emoji!,
sendController
..text = sendController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: sendController.text.length),
);
}
}
// #Pangea
@ -1711,7 +1629,6 @@ class ChatController extends State<ChatPageWithRoom>
selectedEvents.add(event);
});
}
// Pangea#
void clearSingleSelectedEvent() {
if (selectedEvents.length <= 1) {
@ -1785,9 +1702,8 @@ class ChatController extends State<ChatPageWithRoom>
final matches = selectedEvents.where((e) => e.eventId == event.eventId);
if (matches.isNotEmpty) {
setState(() => selectedEvents.remove(matches.first));
}
// Pangea#
else {
// Pangea#
} else {
setState(
() => selectedEvents.add(event),
);
@ -1892,7 +1808,7 @@ class ChatController extends State<ChatPageWithRoom>
}
Timer? _storeInputTimeoutTimer;
Duration storeInputTimeout = const Duration(milliseconds: 500);
static const Duration _storeInputTimeout = Duration(milliseconds: 500);
void onInputBarChanged(String text) {
if (_inputTextIsEmpty != text.isEmpty) {
@ -1902,7 +1818,7 @@ class ChatController extends State<ChatPageWithRoom>
}
_storeInputTimeoutTimer?.cancel();
_storeInputTimeoutTimer = Timer(storeInputTimeout, () async {
_storeInputTimeoutTimer = Timer(_storeInputTimeout, () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('draft_$roomId', text);
});
@ -1946,12 +1862,14 @@ class ChatController extends State<ChatPageWithRoom>
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership);
// #Pangea
// void showEventInfo([Event? event]) =>
// (event ?? selectedEvents.single).showInfoDialog(context);
void showEventInfo([Event? event]) {
(event ?? selectedEvents.single).showInfoDialog(context);
// #Pangea
clearSelectedEvents();
// Pangea#
}
// Pangea#
void onPhoneButtonTap() async {
// VoIP required Android SDK 21
@ -2009,7 +1927,6 @@ class ChatController extends State<ChatPageWithRoom>
replyEvent = null;
editEvent = null;
});
// #Pangea
String? get buttonEventID => timeline!.events
.firstWhereOrNull(
@ -2270,5 +2187,3 @@ class ChatController extends State<ChatPageWithRoom>
);
}
}
enum EmojiPickerType { reaction, keyboard }

@ -23,7 +23,7 @@ class ChatAppBarTitle extends StatelessWidget {
return Text(
controller.selectedEvents.length.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.tertiary,
color: Theme.of(context).colorScheme.onTertiaryContainer,
),
);
}

@ -201,6 +201,10 @@ class ChatEventList extends StatelessWidget {
// Pangea#
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),
singleSelected:
controller.selectedEvents.singleOrNull?.eventId ==
event.eventId,
onEdit: () => controller.editSelectedEventAction(),
timeline: timeline,
displayReadMarker: i > 0 &&
controller.readMarkerEventId == event.eventId,

@ -21,10 +21,7 @@ class ChatInputRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (controller.showEmojiPicker &&
controller.emojiPickerType == EmojiPickerType.reaction) {
return const SizedBox.shrink();
}
const height = 48.0;
if (!controller.room.otherPartyCanReceiveMessages) {
@ -40,6 +37,10 @@ class ChatInputRow extends StatelessWidget {
);
}
final selectedTextButtonStyle = TextButton.styleFrom(
foregroundColor: theme.colorScheme.onTertiaryContainer,
);
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -51,7 +52,7 @@ class ChatInputRow extends StatelessWidget {
height: height,
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.error,
foregroundColor: Colors.orange,
),
onPressed: controller.deleteErrorEventsAction,
child: Row(
@ -66,6 +67,7 @@ class ChatInputRow extends StatelessWidget {
SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.forwardEventsAction,
child: Row(
children: <Widget>[
@ -83,6 +85,7 @@ class ChatInputRow extends StatelessWidget {
? SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.replyAction,
child: Row(
children: <Widget>[
@ -95,6 +98,7 @@ class ChatInputRow extends StatelessWidget {
: SizedBox(
height: height,
child: TextButton(
style: selectedTextButtonStyle,
onPressed: controller.sendAgainAction,
child: Row(
children: <Widget>[
@ -276,8 +280,8 @@ class ChatInputRow extends StatelessWidget {
: null,
// #Pangea
// onSubmitted: controller.onInputBarSubmitted,
onSubmitted: (value) =>
controller.onInputBarSubmitted(value, context),
onSubmitted: (_) =>
controller.onInputBarSubmitted(_, context),
// Pangea#
onSubmitImage: controller.sendImageFromClipBoard,
focusNode: controller.inputFocus,

@ -45,15 +45,6 @@ class ChatView extends StatelessWidget {
tooltip: L10n.of(context).copy,
onPressed: controller.copyEventsAction,
),
if (controller.canSaveSelectedEvent)
// Use builder context to correctly position the share dialog on iPad
Builder(
builder: (context) => IconButton(
icon: Icon(Icons.adaptive.share),
tooltip: L10n.of(context).share,
onPressed: () => controller.saveSelectedEvent(context),
),
),
if (controller.canPinSelectedEvents)
IconButton(
icon: const Icon(Icons.push_pin_outlined),
@ -81,6 +72,19 @@ class ChatView extends StatelessWidget {
}
},
itemBuilder: (context) => [
if (controller.canSaveSelectedEvent)
PopupMenuItem(
onTap: () => controller.saveSelectedEvent(context),
value: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.download_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).downloadFile),
],
),
),
PopupMenuItem(
value: _EventContextAction.info,
child: Row(
@ -193,15 +197,18 @@ class ChatView extends StatelessWidget {
actionsIconTheme: IconThemeData(
color: controller.selectedEvents.isEmpty
? null
: theme.colorScheme.tertiary,
: theme.colorScheme.onTertiaryContainer,
),
backgroundColor: controller.selectedEvents.isEmpty
? null
: theme.colorScheme.tertiaryContainer,
automaticallyImplyLeading: false,
leading: controller.selectMode
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close,
color: theme.colorScheme.tertiary,
color: theme.colorScheme.onTertiaryContainer,
)
: FluffyThemes.isColumnMode(context)
? null
@ -297,12 +304,12 @@ class ChatView extends StatelessWidget {
),
),
SafeArea(
child:
// #Pangea
Stack(
// #Pangea
// child: Column(
child: Stack(
children: [
// Pangea#
Column(
// Pangea#
children: <Widget>[
Expanded(
child: GestureDetector(
@ -310,13 +317,14 @@ class ChatView extends StatelessWidget {
child: ChatEventList(controller: controller),
),
),
if (controller.showScrollDownButton)
Divider(
height: 1,
color: theme.dividerColor,
),
if (controller.room.isExtinct)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
margin: EdgeInsets.all(bottomSheetPadding),
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.chevron_right),
@ -327,18 +335,16 @@ class ChatView extends StatelessWidget {
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
margin: EdgeInsets.all(bottomSheetPadding),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: theme.colorScheme.surfaceContainerHigh,
color: controller.selectedEvents.isNotEmpty
? theme.colorScheme.tertiaryContainer
: theme.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
@ -386,7 +392,6 @@ class ChatView extends StatelessWidget {
// : Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ReactionsPicker(controller),
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),

File diff suppressed because it is too large Load Diff

@ -519,6 +519,7 @@ class InputBar extends StatelessWidget {
// builder: (context, controller, focusNode) => TextField(
// controller: controller,
// focusNode: focusNode,
// readOnly: readOnly,
// contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
// contentInsertionConfiguration: ContentInsertionConfiguration(
// onContentInserted: (KeyboardInsertedContent content) {

@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import '../../config/themes.dart';
class ReactionsPicker extends StatelessWidget {
final ChatController controller;
const ReactionsPicker(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (controller.showEmojiPicker) return const SizedBox.shrink();
final display = controller.editEvent == null &&
controller.replyEvent == null &&
controller.room.canSendDefaultMessages &&
controller.selectedEvents.isNotEmpty;
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// height: (display) ? 56 : 0,
height: (display) ? AppConfig.reactionsPickerHeight : 0,
// Pangea#
child: Material(
color: Colors.transparent,
child: Builder(
builder: (context) {
if (!display) {
return const SizedBox.shrink();
}
final emojis = List<String>.from(AppEmojis.emojis);
final allReactionEvents = controller.selectedEvents.first
.aggregatedEvents(
controller.timeline!,
RelationshipTypes.reaction,
)
.where(
(event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction',
);
for (final event in allReactionEvents) {
try {
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
} catch (_) {}
}
return Row(
children: [
Expanded(
child: Container(
decoration: const BoxDecoration(
// #Pangea
// color: theme.colorScheme.onInverseSurface,
// Pangea#
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(AppConfig.borderRadius),
),
),
padding: const EdgeInsets.only(right: 1),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: emojis.length,
itemBuilder: (c, i) => InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => controller.sendEmojiAction(emojis[i]),
child: Container(
// #Pangea
// width: 56,
// height: 56,
width: AppConfig.reactionsPickerHeight,
height: AppConfig.reactionsPickerHeight,
// Pangea#
alignment: Alignment.center,
child: Text(
emojis[i],
// #Pangea
// style: const TextStyle(fontSize: 30),
style: const TextStyle(fontSize: 20),
// Pangea#
),
),
),
),
),
),
InkWell(
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: 36,
// #Pangea
// height: 56,
height: AppConfig.reactionsPickerHeight,
// Pangea#
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
shape: BoxShape.circle,
),
child: const Icon(Icons.add_outlined),
),
onTap: () =>
controller.pickEmojiReactionAction(allReactionEvents),
),
],
);
},
),
),
);
}
}

@ -21,7 +21,7 @@ class SeenByRow extends StatelessWidget {
alignment: Alignment.center,
child: AnimatedContainer(
constraints:
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth),
height: seenByUsers.isEmpty ? 0 : 24,
duration: seenByUsers.isEmpty
? Duration.zero

@ -34,7 +34,7 @@ class TypingIndicators extends StatelessWidget {
alignment: Alignment.center,
child: AnimatedContainer(
constraints:
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth),
height: typingUsers.isEmpty ? 0 : avatarSize + 8,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,

@ -1177,7 +1177,13 @@ class ChatListController extends State<ChatList>
// if (client.prevBatch == null) {
if (client.onSync.value?.nextBatch == null) {
// Pangea#
await client.onSync.stream.first;
await client.onSyncStatus.stream
.firstWhere((status) => status.status == SyncStatus.finished);
if (!mounted) return;
setState(() {
waitForFirstSync = true;
});
// Display first login bootstrap if enabled
// #Pangea

@ -60,7 +60,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
: status.calcLocalizedString(context),
hintStyle: TextStyle(
color: status.error != null
? theme.colorScheme.error
? Colors.orange
: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
@ -88,8 +88,8 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
strokeWidth: 2,
value: status.progress,
valueColor: status.error != null
? AlwaysStoppedAnimation<Color>(
theme.colorScheme.error,
? const AlwaysStoppedAnimation<Color>(
Colors.orange,
)
: null,
),

@ -69,10 +69,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
if (homeserverInput.isEmpty) {
final client = await Matrix.of(context).getLoginClient();
setState(() {
error = loginFlows = null;
isLoading = false;
Matrix.of(context).getLoginClient().homeserver = null;
client.homeserver = null;
});
return;
}
@ -88,7 +89,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeserverInput, '');
}
final client = Matrix.of(context).getLoginClient();
final client = await Matrix.of(context).getLoginClient();
final (_, _, loginFlows) = await client.checkHomeserver(homeserver);
this.loginFlows = loginFlows;
if (supportsSso && !legacyPasswordLogin) {
@ -105,6 +106,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
}
context.push(
'${GoRouter.of(context).routeInformationProvider.value.uri.path}/login',
extra: client,
);
} catch (e) {
setState(
@ -142,8 +144,8 @@ class HomeserverPickerController extends State<HomeserverPicker> {
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url = Matrix.of(context).getLoginClient().homeserver!.replace(
final client = await Matrix.of(context).getLoginClient();
final url = client.homeserver!.replace(
path: '/_matrix/client/v3/login/sso/redirect',
queryParameters: {'redirectUrl': redirectUrl},
);
@ -164,11 +166,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
isLoading = true;
});
try {
await Matrix.of(context).getLoginClient().login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
);
await client.login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
);
} catch (e) {
setState(() {
error = e.toLocalizedString(context);
@ -200,7 +202,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
isLoading = true;
});
try {
final client = Matrix.of(context).getLoginClient();
final client = await Matrix.of(context).getLoginClient();
await client.importDump(String.fromCharCodes(await file.readAsBytes()));
Matrix.of(context).initMatrix();
} catch (e) {

@ -25,7 +25,8 @@ class HomeserverPickerView extends StatelessWidget {
final theme = Theme.of(context);
return LoginScaffold(
enforceMobileMode: Matrix.of(context).client.isLogged(),
enforceMobileMode:
Matrix.of(context).widget.clients.any((client) => client.isLogged()),
appBar: AppBar(
centerTitle: true,
title: Text(

@ -16,7 +16,8 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class Login extends StatefulWidget {
const Login({super.key});
final Client client;
const Login({required this.client, super.key});
@override
LoginController createState() => LoginController();
@ -132,17 +133,18 @@ class LoginController extends State<Login> {
// } else {
// identifier = AuthenticationUserIdentifier(user: username);
// }
// await matrix.getLoginClient().login(
// LoginType.mLoginPassword,
// identifier: identifier,
// // To stay compatible with older server versions
// // ignore: deprecated_member_use
// user: identifier.type == AuthenticationIdentifierTypes.userId
// ? username
// : null,
// password: passwordController.text,
// initialDeviceDisplayName: PlatformInfos.clientName,
// );
// final client = await matrix.getLoginClient();
// client.login(
// LoginType.mLoginPassword,
// identifier: identifier,
// // To stay compatible with older server versions
// // ignore: deprecated_member_use
// user: identifier.type == AuthenticationIdentifierTypes.userId
// ? username
// : null,
// password: passwordController.text,
// initialDeviceDisplayName: PlatformInfos.clientName,
// );
// } on MatrixException catch (exception) {
// setState(() => passwordError = exception.errorMessage);
// return setState(() => loading = false);
@ -168,14 +170,13 @@ class LoginController extends State<Login> {
void _checkWellKnown(String userId) async {
if (mounted) setState(() => usernameError = null);
if (!userId.isValidMatrixId) return;
final oldHomeserver = Matrix.of(context).getLoginClient().homeserver;
final oldHomeserver = widget.client.homeserver;
try {
var newDomain = Uri.https(userId.domain!, '');
Matrix.of(context).getLoginClient().homeserver = newDomain;
widget.client.homeserver = newDomain;
DiscoveryInformation? wellKnownInformation;
try {
wellKnownInformation =
await Matrix.of(context).getLoginClient().getWellknown();
wellKnownInformation = await widget.client.getWellknown();
if (wellKnownInformation.mHomeserver.baseUrl.toString().isNotEmpty) {
newDomain = wellKnownInformation.mHomeserver.baseUrl;
}
@ -183,10 +184,10 @@ class LoginController extends State<Login> {
// do nothing, newDomain is already set to a reasonable fallback
}
if (newDomain != oldHomeserver) {
await Matrix.of(context).getLoginClient().checkHomeserver(newDomain);
await widget.client.checkHomeserver(newDomain);
if (Matrix.of(context).getLoginClient().homeserver == null) {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
if (widget.client.homeserver == null) {
widget.client.homeserver = oldHomeserver;
// okay, the server we checked does not appear to be a matrix server
Logs().v(
'$newDomain is not running a homeserver, asking to use $oldHomeserver',
@ -209,13 +210,13 @@ class LoginController extends State<Login> {
usernameError = null;
if (mounted) setState(() {});
} else {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
widget.client.homeserver = oldHomeserver;
if (mounted) {
setState(() {});
}
}
} catch (e) {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
widget.client.homeserver = oldHomeserver;
usernameError = e.toLocalizedString(context);
if (mounted) setState(() {});
}
@ -238,12 +239,11 @@ class LoginController extends State<Login> {
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
final response = await showFutureLoadingDialog(
context: context,
future: () =>
Matrix.of(context).getLoginClient().requestTokenToResetPasswordEmail(
clientSecret,
input,
sendAttempt++,
),
future: () => widget.client.requestTokenToResetPasswordEmail(
clientSecret,
input,
sendAttempt++,
),
);
if (response.error != null) return;
final password = await showTextInputDialog(
@ -283,11 +283,11 @@ class LoginController extends State<Login> {
};
final success = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).getLoginClient().request(
RequestType.POST,
'/client/v3/account/password',
data: data,
),
future: () => widget.client.request(
RequestType.POST,
'/client/v3/account/password',
data: data,
),
);
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(

@ -14,16 +14,15 @@ class LoginView extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final homeserver = Matrix.of(context)
.getLoginClient()
.homeserver
final homeserver = controller.widget.client.homeserver
.toString()
.replaceFirst('https://', '');
final title = L10n.of(context).logInTo(homeserver);
final titleParts = title.split(homeserver);
return LoginScaffold(
enforceMobileMode: Matrix.of(context).client.isLogged(),
enforceMobileMode:
Matrix.of(context).widget.clients.any((client) => client.isLogged()),
appBar: AppBar(
leading:
controller.loadingSignIn ? null : const Center(child: BackButton()),

@ -266,7 +266,7 @@ class ActivityPlanMessage extends StatelessWidget {
onSwipe: (_) {},
child: Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
maxWidth: FluffyThemes.maxTimelineWidth,
),
padding: const EdgeInsets.only(
left: 8.0,

@ -80,7 +80,6 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
_analyticsStream = null;
_languageStream?.cancel();
_languageStream = null;
_refreshAnalyticsIfOutdated();
clearMessagesSinceUpdate();
}

@ -55,7 +55,7 @@ class ChatInputBarState extends State<ChatInputBar> {
right: widget.padding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child: Material(

@ -124,10 +124,6 @@ class PangeaChatInputRowState extends State<PangeaChatInputRow> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (_controller.showEmojiPicker &&
_controller.emojiPickerType == EmojiPickerType.reaction) {
return const SizedBox.shrink();
}
const height = 48.0;
return Column(

@ -7,6 +7,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/contextual_definition_controller.dart';
@ -24,7 +25,6 @@ import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.
import 'package:fluffychat/pangea/user/controllers/permissions_controller.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart';
import '../../choreographer/controllers/it_feedback_controller.dart';
import '../utils/firebase_analytics.dart';
@ -132,7 +132,8 @@ class PangeaController {
}
Future<void> checkHomeServerAction() async {
if (matrixState.getLoginClient().homeserver != null) {
final client = await matrixState.getLoginClient();
if (client.homeserver != null) {
await Future.delayed(Duration.zero);
return;
}
@ -145,7 +146,7 @@ class PangeaController {
}
try {
await matrixState.getLoginClient().register();
await client.register();
matrixState.loginRegistrationSupported = true;
} on MatrixException catch (e) {
matrixState.loginRegistrationSupported =
@ -156,7 +157,7 @@ class PangeaController {
}
/// check user information if not found then redirect to Date of birth page
_handleLoginStateChange(LoginState state) {
_handleLoginStateChange(LoginState state, String? userID) {
switch (state) {
case LoginState.loggedOut:
case LoginState.softLoggedOut:
@ -180,12 +181,12 @@ class PangeaController {
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: matrixState.client.userID,
name: matrixState.client.userID,
id: userID,
name: userID,
),
),
);
GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID);
GoogleAnalytics.analyticsUserUpdate(userID);
}
Future<void> resetAnalytics() async {
@ -196,8 +197,9 @@ class PangeaController {
}
void _subscribeToStreams() {
final userID = matrixState.client.userID;
matrixState.client.onLoginStateChanged.stream
.listen(_handleLoginStateChange);
.listen((state) => _handleLoginStateChange(state, userID));
_setLanguageStream();
}

@ -16,7 +16,10 @@ class PAuthGaurd {
GoRouterState state,
) async {
if (pController != null) {
if (Matrix.of(context).client.isLogged()) {
if (Matrix.of(context)
.widget
.clients
.any((client) => client.isLogged())) {
final bool dobIsSet =
await pController!.userController.isUserDataAvailableAndL2Set;
return dobIsSet ? '/rooms' : '/user_age';
@ -34,7 +37,10 @@ class PAuthGaurd {
GoRouterState state,
) async {
if (pController != null) {
if (!Matrix.of(context).client.isLogged()) {
if (!Matrix.of(context)
.widget
.clients
.any((client) => client.isLogged())) {
return '/home';
}
final bool dobIsSet =

@ -39,7 +39,8 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
}
}
void setEmoji(String emoji) => widget.controller.sendEmojiAction(emoji);
void setEmoji(String emoji) {}
// widget.controller.sendEmojiAction(emoji);
Future<void> _refresh() async {
setState(() {

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart';
import 'package:fluffychat/pangea/login/widgets/app_config_dialog.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LoginOrSignupView extends StatefulWidget {
const LoginOrSignupView({super.key});
@ -16,12 +18,17 @@ class LoginOrSignupView extends StatefulWidget {
}
class LoginOrSignupViewState extends State<LoginOrSignupView> {
Client? client;
List<AppConfigOverride> _overrides = [];
@override
void initState() {
super.initState();
_loadOverrides();
Matrix.of(context).getLoginClient().then((c) {
if (mounted) setState(() => client = c);
});
}
Future<void> _loadOverrides() async {
@ -61,7 +68,12 @@ class LoginOrSignupViewState extends State<LoginOrSignupView> {
),
FullWidthButton(
title: L10n.of(context).signIn,
onPressed: () => context.go('/home/login'),
onPressed: client != null
? () => context.go(
'/home/login',
extra: Matrix.of(context).client,
)
: null,
),
],
);

@ -171,7 +171,7 @@ class SignupPageController extends State<SignupPage> {
}
Future<void> _signupFuture() async {
final client = Matrix.of(context).getLoginClient();
final client = await Matrix.of(context).getLoginClient();
final email = emailController.text;
if (email.isNotEmpty) {
Matrix.of(context).currentClientSecret =

@ -16,12 +16,10 @@ import 'package:fluffychat/widgets/matrix.dart';
Future<void> pangeaSSOLoginAction(
IdentityProvider provider,
Client client,
BuildContext context,
) async {
final bool isDefaultPlatform =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
final redirectUrl = kIsWeb
? Uri.parse(html.window.location.href)
.resolveUri(
@ -31,16 +29,15 @@ Future<void> pangeaSSOLoginAction(
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url = Matrix.of(context).getLoginClient().homeserver!.replace(
path: '/_matrix/client/v3/login/sso/redirect/${provider.id ?? ''}',
final client = await Matrix.of(context).getLoginClient();
final url = client.homeserver!.replace(
path: '/_matrix/client/v3/login/sso/redirect',
queryParameters: {'redirectUrl': redirectUrl},
);
final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
String result;
try {
result = await FlutterWebAuth2.authenticate(
@ -54,7 +51,6 @@ Future<void> pangeaSSOLoginAction(
}
rethrow;
}
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;

@ -8,7 +8,6 @@ import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pangea/login/utils/sso_login_action.dart';
import 'package:fluffychat/pangea/login/widgets/full_width_button.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum SSOProvider { google, apple }
@ -67,7 +66,6 @@ class PangeaSsoButton extends StatelessWidget {
id: provider.id,
name: provider.name,
),
Matrix.of(context).getLoginClient(),
context,
),
onError: (e, s) {

@ -533,7 +533,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
const double messageMargin = 16.0;
// widget.event.isActivityMessage ? 0 : Avatar.defaultSize + 16 + 8;
final bool showingDetails = widget.chatController.displayChatDetailsColumn;
final double totalMaxWidth = (FluffyThemes.columnWidth * 2.5) -
final double totalMaxWidth = FluffyThemes.maxTimelineWidth -
(showingDetails ? FluffyThemes.columnWidth : 0) -
messageMargin;
double? maxWidth;

@ -6,7 +6,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
class OverlayHeader extends StatefulWidget {
@ -118,14 +117,14 @@ class OverlayHeaderState extends State<OverlayHeader> {
color: theme.colorScheme.primary,
),
if (controller.canEditSelectedEvents &&
!controller.selectedEvents.first.isActivityMessage)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: l10n.edit,
onPressed: controller.editSelectedEventAction,
color: theme.colorScheme.primary,
),
// if (controller.canEditSelectedEvents &&
// !controller.selectedEvents.first.isActivityMessage)
// IconButton(
// icon: const Icon(Icons.edit_outlined),
// tooltip: l10n.edit,
// onPressed: controller.editSelectedEventAction,
// color: theme.colorScheme.primary,
// ),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),

@ -63,7 +63,7 @@ Future<void> _loginFuture({
identifier = AuthenticationUserIdentifier(user: username);
}
final client = matrix.getLoginClient();
final client = await matrix.getLoginClient();
final redirect = client.onLoginStateChanged.stream
.where((state) => state == LoginState.loggedIn)

@ -1,153 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/login/login.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../../widgets/matrix.dart';
extension PangeaPasswordForgotten on LoginController {
void pangeaPasswordForgotten() async {
final TextEditingController emailController = TextEditingController();
final TextEditingController newPasswordController = TextEditingController();
showDialog(
context: context,
useRootNavigator: false,
builder: (BuildContext context) => Scaffold(
backgroundColor: Colors.transparent,
body: AlertDialog(
title: Text(L10n.of(context).passwordForgotten),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.of(context).enterAnEmailAddress),
const SizedBox(height: 12),
TextField(
controller: emailController,
decoration: InputDecoration(
hintText: L10n.of(context).enterAnEmailAddress,
),
),
],
),
actions: [
TextButton(
child: Text(L10n.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
return;
},
),
TextButton(
child: Text(L10n.of(context).ok),
onPressed: () async {
if (emailController.text == "") return;
final clientSecret =
DateTime.now().millisecondsSinceEpoch.toString();
final response = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context)
.getLoginClient()
.requestTokenToResetPasswordEmail(
clientSecret,
emailController.text,
LoginController.sendAttempt++,
),
);
if (response.error != null) {
return;
}
Navigator.of(context).pop();
showDialog(
context: context,
useRootNavigator: false,
builder: (BuildContext context) => Scaffold(
backgroundColor: Colors.transparent,
body: AlertDialog(
title: Text(L10n.of(context).passwordForgotten),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.of(context).chooseAStrongPassword),
const SizedBox(height: 12),
TextField(
obscureText: true,
controller: newPasswordController,
decoration: const InputDecoration(
hintText: "******",
),
),
],
),
actions: [
TextButton(
child: Text(L10n.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
return;
},
),
TextButton(
child: Text(L10n.of(context).ok),
onPressed: () async {
if (newPasswordController.text == "") return;
final ok = await showOkAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).weSentYouAnEmail,
// #Pangea
// message: L10n.of(context).pleaseClickOnLink,
message: L10n.of(context).clickOnEmailLink,
// Pangea#
okLabel: L10n.of(context).iHaveClickedOnLink,
);
if (ok != OkCancelResult.ok) return;
final data = <String, dynamic>{
'new_password': newPasswordController.text,
'logout_devices': false,
"auth": AuthenticationThreePidCreds(
type: AuthenticationTypes.emailIdentity,
threepidCreds: ThreepidCreds(
sid: response.result!.sid,
clientSecret: clientSecret,
),
).toJson(),
};
final success = await showFutureLoadingDialog(
context: context,
future: () =>
Matrix.of(context).getLoginClient().request(
RequestType.POST,
'/client/r0/account/password',
data: data,
),
);
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).passwordHasBeenChanged,
),
),
);
usernameController.text = emailController.text;
passwordController.text =
newPasswordController.text;
login();
}
},
),
],
),
),
);
},
),
],
),
),
);
}
}

@ -25,7 +25,7 @@ extension ClientDownloadContentExtension on Client {
)
: mxc;
final cachedData = await database?.getFile(cacheKey);
final cachedData = await database.getFile(cacheKey);
if (cachedData != null) return cachedData;
final httpUri = isThumbnail
@ -55,7 +55,7 @@ extension ClientDownloadContentExtension on Client {
}
}
await database?.storeFile(cacheKey, imageData, 0);
await database.storeFile(cacheKey, imageData, 0);
return imageData;
}

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:desktop_notifications/desktop_notifications.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/encryption/utils/key_verification.dart';
import 'package:matrix/matrix.dart';
@ -47,7 +48,7 @@ abstract class ClientManager {
await store.setStringList(clientNamespace, clientNames.toList());
}
final clients =
clientNames.map((name) => createClient(name, store)).toList();
await Future.wait(clientNames.map((name) => createClient(name, store)));
if (initialize) {
await Future.wait(
clients.map(
@ -101,9 +102,15 @@ abstract class ClientManager {
static NativeImplementations get nativeImplementations => kIsWeb
? const NativeImplementationsDummy()
: NativeImplementationsIsolate(compute);
: NativeImplementationsIsolate(
compute,
vodozemacInit: () => vod.init(wasmPath: './assets/assets/vodozemac/'),
);
static Client createClient(String clientName, SharedPreferences store) {
static Future<Client> createClient(
String clientName,
SharedPreferences store,
) async {
final shareKeysWith = AppSettings.shareKeysWith.getItem(store);
final enableSoftLogout = AppSettings.enableSoftLogout.getItem(store);
@ -133,7 +140,7 @@ abstract class ClientManager {
// Pangea#
},
logLevel: kReleaseMode ? Level.warning : Level.verbose,
databaseBuilder: flutterMatrixSdkDatabaseBuilder,
database: await flutterMatrixSdkDatabaseBuilder(clientName),
supportedLoginTypes: {
AuthenticationTypes.password,
AuthenticationTypes.sso,

@ -32,11 +32,11 @@ extension LocalizedBody on Event {
bool get isAttachmentSmallEnough =>
infoMap['size'] is int &&
infoMap['size'] < room.client.database!.maxFileSize;
infoMap['size'] < room.client.database.maxFileSize;
bool get isThumbnailSmallEnough =>
thumbnailInfoMap['size'] is int &&
thumbnailInfoMap['size'] < room.client.database!.maxFileSize;
thumbnailInfoMap['size'] < room.client.database.maxFileSize;
bool get showThumbnail =>
[MessageTypes.Image, MessageTypes.Sticker, MessageTypes.Video]

@ -16,10 +16,10 @@ import 'cipher.dart';
import 'sqlcipher_stub.dart'
if (dart.library.io) 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(String clientName) async {
MatrixSdkDatabase? database;
try {
database = await _constructDatabase(client);
database = await _constructDatabase(clientName);
await database.open();
return database;
} catch (e, s) {
@ -27,7 +27,7 @@ Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
ErrorHandler.logError(
e: e,
s: s,
data: {"clientID": client.id},
data: {"clientID": clientName},
m: "Failed to open matrix sdk database. Openning fallback database.",
);
// Pangea#
@ -53,7 +53,7 @@ Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
// Delete database file:
if (database == null && !kIsWeb) {
final dbFile = File(await _getDatabasePath(client.clientName));
final dbFile = File(await _getDatabasePath(clientName));
if (await dbFile.exists()) await dbFile.delete();
}
@ -77,10 +77,10 @@ Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(Client client) async {
}
}
Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
Future<MatrixSdkDatabase> _constructDatabase(String clientName) async {
if (kIsWeb) {
html.window.navigator.storage?.persist();
return MatrixSdkDatabase(client.clientName);
return await MatrixSdkDatabase.init(clientName);
}
final cipher = await getDatabaseCipher();
@ -97,7 +97,7 @@ Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
);
}
final path = await _getDatabasePath(client.clientName);
final path = await _getDatabasePath(clientName);
// fix dlopen for old Android
await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
@ -109,7 +109,7 @@ Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
// Pangea#
// migrate from potential previous SQLite database path to current one
await _migrateLegacyLocation(path, client.clientName);
await _migrateLegacyLocation(path, clientName);
// required for [getDatabasesPath]
databaseFactory = factory;
@ -139,8 +139,8 @@ Future<MatrixSdkDatabase> _constructDatabase(Client client) async {
),
);
return MatrixSdkDatabase(
client.clientName,
return await MatrixSdkDatabase.init(
clientName,
database: database,
maxFileSize: 1000 * 1000 * 10,
fileStorageLocation: fileStorageLocation?.uri,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save