commit
326f100520
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,6 +11,7 @@
|
|||
.svn/
|
||||
lib/generated_plugin_registrant.dart
|
||||
google-services.json
|
||||
prime
|
||||
|
||||
# libolm package
|
||||
/assets/js/package/*
|
||||
|
|
|
@ -158,8 +158,7 @@ upload_to_fdroid_repo:
|
|||
- export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk"
|
||||
- rsync -rav -e ssh ./ fluffy@fdroid.nordgedanken.dev:/fdroid/repo
|
||||
- ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && fdroid update"
|
||||
dependencies:
|
||||
- build_android_apk
|
||||
needs: ["build_android_apk"]
|
||||
only:
|
||||
- tags
|
||||
|
||||
|
@ -174,8 +173,7 @@ pages:
|
|||
- cd build/web/ && bundle install && cd ../../
|
||||
- cd build/web/ && bundle exec jekyll build -d public && cd ../../
|
||||
- mv build/web/public ./
|
||||
dependencies:
|
||||
- build_web
|
||||
needs: ["build_web"]
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
@ -215,3 +213,4 @@ snap:publish:
|
|||
- './*.snap'
|
||||
when: on_success
|
||||
expire_in: 1 week
|
||||
needs: []
|
||||
|
|
13
README.md
13
README.md
|
@ -3,10 +3,12 @@
|
|||
An experimental fork of FluffyChat.
|
||||
|
||||
# Changes from FluffyChat
|
||||
* Swipe to reply (or forward/edit)
|
||||
* Reworked auth flow
|
||||
* Removed Sentry
|
||||
* Double check of .well-known
|
||||
* Get Jitsi instance from .well-known
|
||||
* Redesigned settings
|
||||
|
||||
# Features
|
||||
* Single and group chats
|
||||
|
@ -35,8 +37,8 @@ An experimental fork of FluffyChat.
|
|||
|
||||
2. Clone the repo:
|
||||
```
|
||||
git clone --recurse-submodules https://gitlab.com/ChristianPauly/fluffychat-flutter
|
||||
cd fluffychat-flutter
|
||||
git clone --recurse-submodules https://github.com/innereq/FurryChat.git
|
||||
cd FurryChat
|
||||
```
|
||||
|
||||
3. Choose your target platform below and enable support for it.
|
||||
|
@ -80,13 +82,6 @@ flutter build windows --release
|
|||
flutter build macos --release
|
||||
```
|
||||
|
||||
|
||||
### Docker
|
||||
|
||||
Don't even ask.
|
||||
|
||||
`docker run -ti --privileged -v /dev/bus/usb:/dev/bus/usb -v ${PWD}:/build -v /home/inex/.pub-cache:/home/inex/.pub-cache -v /home/inex/flutter:/home/inex/flutter -d flutter-fluffy:1.0`
|
||||
|
||||
## How to add translations for your language
|
||||
|
||||
You can use Weblate to translate the app to your language:
|
||||
|
|
|
@ -2,3 +2,4 @@ arb-dir: lib/l10n
|
|||
template-arb-file: intl_en.arb
|
||||
output-localization-file: l10n.dart
|
||||
output-class: L10n
|
||||
preferred-supported-locales: ["en"]
|
|
@ -11,7 +11,6 @@ import 'package:furrychat/utils/user_status.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:localstorage/localstorage.dart';
|
||||
import 'package:universal_html/prefer_universal/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
@ -72,13 +71,15 @@ class MatrixState extends State<Matrix> {
|
|||
File wallpaper;
|
||||
bool renderHtml = false;
|
||||
|
||||
String swipeToEndAction;
|
||||
String swipeToStartAction = 'reply';
|
||||
|
||||
String jitsiInstance = 'https://meet.jit.si/';
|
||||
|
||||
void clean() async {
|
||||
if (!kIsWeb) return;
|
||||
|
||||
final storage = LocalStorage('LocalStorage');
|
||||
await storage.ready;
|
||||
final storage = await getLocalStorage();
|
||||
await storage.deleteItem(widget.clientName);
|
||||
}
|
||||
|
||||
|
@ -285,6 +286,16 @@ class MatrixState extends State<Matrix> {
|
|||
store.getItem('chat.fluffy.renderHtml').then((final render) async {
|
||||
renderHtml = render == '1';
|
||||
});
|
||||
store
|
||||
.getItem('dev.inex.furrychat.swipeToEndAction')
|
||||
.then((final action) async {
|
||||
swipeToEndAction = action ?? swipeToEndAction;
|
||||
});
|
||||
store
|
||||
.getItem('dev.inex.furrychat.swipeToStartAction')
|
||||
.then((final action) async {
|
||||
swipeToStartAction = action ?? swipeToStartAction;
|
||||
});
|
||||
}
|
||||
if (kIsWeb) {
|
||||
onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true);
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheDisplaynameTo": "{username} غيّر اسمه الى {displayname}",
|
||||
"changedTheDisplaynameTo": "{username} غيّر اسمه العلني الى {displayname}",
|
||||
"@changedTheDisplaynameTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
|
@ -375,7 +375,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"couldNotSetDisplayname": "تعذر تعيين الاسم",
|
||||
"couldNotSetDisplayname": "تعذر تعيين الاسم العلني",
|
||||
"@couldNotSetDisplayname": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
@ -489,7 +489,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"displaynameHasBeenChanged": "غُيِّر الاسم",
|
||||
"displaynameHasBeenChanged": "غُيِّر الاسم العلني",
|
||||
"@displaynameHasBeenChanged": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
@ -499,7 +499,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"editDisplayname": "حرر الاسم",
|
||||
"editDisplayname": "حرر الاسم العلني",
|
||||
"@editDisplayname": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
@ -514,7 +514,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة",
|
||||
"emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة!",
|
||||
"@emoteWarnNeedToPick": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
@ -1374,7 +1374,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"useAmoledTheme": "",
|
||||
"useAmoledTheme": "هل تريد استخدم ألوان متوافقة مع Amoled؟",
|
||||
"@useAmoledTheme": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
@ -1507,14 +1507,14 @@
|
|||
"unreadCount": {}
|
||||
}
|
||||
},
|
||||
"unreadMessages": "",
|
||||
"unreadMessages": "{unreadEvents} رسالة غير مقروءة",
|
||||
"@unreadMessages": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"unreadEvents": {}
|
||||
}
|
||||
},
|
||||
"unreadMessagesInChats": "",
|
||||
"unreadMessagesInChats": "{unreadEvents} رسالة غير مقروءة من {unreadChats} محادثة",
|
||||
"@unreadMessagesInChats": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
|
@ -1702,5 +1702,10 @@
|
|||
"@yourOwnUsername": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"privacy": "الخصوصية",
|
||||
"@privacy": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -514,6 +514,11 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"edit": "Edit",
|
||||
"@edit": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"editDisplayname": "Edit displayname",
|
||||
"@editDisplayname": {
|
||||
"type": "text",
|
||||
|
@ -1439,6 +1444,16 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"swipeToEndAction": "Swipe to right action",
|
||||
"@swipeToEndAction": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"swipeToStartAction": "Swipe to left action",
|
||||
"@swipeToStartAction": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"donate": "Donate",
|
||||
"@donate": {
|
||||
"type": "text",
|
||||
|
|
|
@ -194,5 +194,272 @@
|
|||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"deleteMessage": "Cancella messaggio",
|
||||
"@deleteMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"deleteAccount": "Elimina account",
|
||||
"@deleteAccount": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"deactivateAccountWarning": "Disabiliterà il tuo account. Non puoi tornare indietro! Sei sicuro?",
|
||||
"@deactivateAccountWarning": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"delete": "Cancella",
|
||||
"@delete": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dateWithYear": "{day}-{month}-{year}",
|
||||
"@dateWithYear": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"year": {},
|
||||
"month": {},
|
||||
"day": {}
|
||||
}
|
||||
},
|
||||
"dateWithoutYear": "{month}-{day}",
|
||||
"@dateWithoutYear": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"month": {},
|
||||
"day": {}
|
||||
}
|
||||
},
|
||||
"dateAndTimeOfDay": "{date}, {timeOfDay}",
|
||||
"@dateAndTimeOfDay": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"date": {},
|
||||
"timeOfDay": {}
|
||||
}
|
||||
},
|
||||
"currentlyActive": "Attualmente attivo",
|
||||
"@currentlyActive": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createNewGroup": "Crea un nuovo gruppo",
|
||||
"@createNewGroup": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createdTheChat": "{username} ha creato la chat",
|
||||
"@createdTheChat": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"createAccountNow": "Crea ora un account",
|
||||
"@createAccountNow": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"create": "Crea",
|
||||
"@create": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"countParticipants": "{count} partecipanti",
|
||||
"@countParticipants": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"couldNotSetDisplayname": "Impossibile impostare nome",
|
||||
"@couldNotSetDisplayname": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"couldNotSetAvatar": "Impossibile impostare avatar",
|
||||
"@couldNotSetAvatar": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"couldNotDecryptMessage": "Impossibile decriptare messaggio: {error}",
|
||||
"@couldNotDecryptMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"copy": "Copia",
|
||||
"@copy": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"copiedToClipboard": "Copiato negli Appunti",
|
||||
"@copiedToClipboard": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"contentViewer": "Visualizzatore contenuti",
|
||||
"@contentViewer": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"contactHasBeenInvitedToTheGroup": "Il contatto è stato invitato nel gruppo",
|
||||
"@contactHasBeenInvitedToTheGroup": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"connectionAttemptFailed": "Tentativo di connessione fallito",
|
||||
"@connectionAttemptFailed": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"connect": "Connetti",
|
||||
"@connect": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"confirm": "Conferma",
|
||||
"@confirm": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"compareNumbersMatch": "Confronta e assicurati che le seguenti emoji corrispondano a quelle dell'altro dispositivo:",
|
||||
"@compareNumbersMatch": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"compareEmojiMatch": "Confronta e assicurati che le seguenti emoji corrispondano a quelle dell'altro dispositivo:",
|
||||
"@compareEmojiMatch": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"close": "Chiudi",
|
||||
"@close": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"chooseAUsername": "Scegli un username",
|
||||
"@chooseAUsername": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"chooseAStrongPassword": "Scegli una password complessa",
|
||||
"@chooseAStrongPassword": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"chatDetails": "Dettagli chat",
|
||||
"@chatDetails": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"chat": "Chat",
|
||||
"@chat": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"channelCorruptedDecryptError": "La crittografia è corrotta",
|
||||
"@channelCorruptedDecryptError": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeTheServer": "Cambia server",
|
||||
"@changeTheServer": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeWallpaper": "Cambia sfondo",
|
||||
"@changeWallpaper": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeTheNameOfTheGroup": "Cambia il nome del gruppo",
|
||||
"@changeTheNameOfTheGroup": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changelog": "Registro cambiamenti",
|
||||
"@changelog": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changedTheRoomInvitationLink": "{username} ha cambiato il link di invito",
|
||||
"@changedTheRoomInvitationLink": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheRoomAliases": "{username} ha cambiato il nome delle stanze",
|
||||
"@changedTheRoomAliases": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheProfileAvatar": "{username} ha cambiato il loro avatar",
|
||||
"@changedTheProfileAvatar": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheJoinRulesTo": "{username} ha cambiato le regole per unirsi in: {joinRules}",
|
||||
"@changedTheJoinRulesTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"joinRules": {}
|
||||
}
|
||||
},
|
||||
"changedTheJoinRules": "{username} ha cambiato le regole per unirsi",
|
||||
"@changedTheJoinRules": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheHistoryVisibilityTo": "{username} ha cambiato la visibilità della cronologia in: {rules}",
|
||||
"@changedTheHistoryVisibilityTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
"changedTheHistoryVisibility": "{username} ha cambiato la visibilità della cronologia",
|
||||
"@changedTheHistoryVisibility": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheGuestAccessRulesTo": "{username} ha cambiato le regole di accesso per ospiti con: {rules}",
|
||||
"@changedTheGuestAccessRulesTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
"changedTheChatAvatar": "{username} ha cambiato avatar",
|
||||
"@changedTheChatAvatar": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"askSSSSSign": "Per entrare con l'altro utente, per favore inserisci la tua passphrase o recovery key.",
|
||||
"@askSSSSSign": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askSSSSCache": "Per favore inserisci la tua passphrase o recovery key per la cache delle chiavi.",
|
||||
"@askSSSSCache": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,11 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"avatar": "Аватар",
|
||||
"@avatar": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"avatarHasBeenChanged": "Аватар был изменён",
|
||||
"@avatarHasBeenChanged": {
|
||||
"type": "text",
|
||||
|
@ -197,6 +202,11 @@
|
|||
"displayname": {}
|
||||
}
|
||||
},
|
||||
"changeThePassword": "Сменить пароль",
|
||||
"@changeThePassword": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changedTheGuestAccessRules": "{username} изменил(а) правила гостевого доступа",
|
||||
"@changedTheGuestAccessRules": {
|
||||
"type": "text",
|
||||
|
@ -509,6 +519,11 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"edit": "Редактировать",
|
||||
"@edit": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"editDisplayname": "Отображаемое имя",
|
||||
"@editDisplayname": {
|
||||
"type": "text",
|
||||
|
@ -681,11 +696,21 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"homeserver": "Сервер Matrix",
|
||||
"@homeserver": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"homeserverIsNotCompatible": "Несовместимый сервер Matrix",
|
||||
"@homeserverIsNotCompatible": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"homeserverOrMXID": "Сервер или полное имя пользователя",
|
||||
"@homeserverOrMXID": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"id": "ID",
|
||||
"@id": {
|
||||
"type": "text",
|
||||
|
@ -1372,6 +1397,16 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"swipeToEndAction": "Действие по жесту вправо",
|
||||
"@swipeToEndAction": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"swipeToStartAction": "Действие по жесту влево",
|
||||
"@swipeToStartAction": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"systemTheme": "Системная",
|
||||
"@systemTheme": {
|
||||
"type": "text",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:localstorage/localstorage.dart';
|
||||
import 'package:universal_html/prefer_universal/html.dart' as html;
|
||||
|
||||
import 'components/matrix.dart';
|
||||
|
@ -21,8 +20,6 @@ void main() {
|
|||
runZonedGuarded(
|
||||
() => runApp(App()),
|
||||
(error, stackTrace) async {
|
||||
final storage = LocalStorage('LocalStorage');
|
||||
await storage.ready;
|
||||
debugPrint(error.toString());
|
||||
debugPrint(stackTrace.toString());
|
||||
},
|
||||
|
|
|
@ -6,12 +6,22 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:localstorage/localstorage.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
import './database/shared.dart';
|
||||
import 'package:olm/olm.dart' as olm; // needed for migration
|
||||
import 'package:random_string/random_string.dart';
|
||||
|
||||
Future<LocalStorage> getLocalStorage() async {
|
||||
final directory = PlatformInfos.isBetaDesktop
|
||||
? await getApplicationSupportDirectory()
|
||||
: await getApplicationDocumentsDirectory();
|
||||
final localStorage = LocalStorage('LocalStorage', directory.path);
|
||||
await localStorage.ready;
|
||||
return localStorage;
|
||||
}
|
||||
|
||||
Future<Database> getDatabase(Client client) async {
|
||||
while (_generateDatabaseLock) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
|
|
@ -18,8 +18,8 @@ Future<MatrixImageFile> resizeImage(MatrixImageFile file,
|
|||
try {
|
||||
final nativeImg = native.Image();
|
||||
await nativeImg.loadEncoded(file.bytes);
|
||||
file.width = nativeImg.width();
|
||||
file.height = nativeImg.height();
|
||||
file.width = nativeImg.width;
|
||||
file.height = nativeImg.height;
|
||||
args = _IsolateArgs(
|
||||
width: file.width, height: file.height, bytes: file.bytes, max: max);
|
||||
nativeImg.free();
|
||||
|
@ -96,8 +96,8 @@ Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async {
|
|||
final ret = _IsolateResponse(
|
||||
blurhash: blurhash,
|
||||
jpegBytes: jpegBytes,
|
||||
width: nativeImg.width(),
|
||||
height: nativeImg.height());
|
||||
width: nativeImg.width,
|
||||
height: nativeImg.height);
|
||||
|
||||
nativeImg.free();
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:swipe_to_action/swipe_to_action.dart';
|
||||
|
||||
import '../components/dialogs/send_file_dialog.dart';
|
||||
import '../components/input_bar.dart';
|
||||
|
@ -316,8 +317,10 @@ class _ChatState extends State<_Chat> {
|
|||
return true;
|
||||
}
|
||||
|
||||
void forwardEventsAction(BuildContext context) async {
|
||||
if (selectedEvents.length == 1) {
|
||||
void forwardEventsAction(BuildContext context, {Event event}) async {
|
||||
if (event != null) {
|
||||
Matrix.of(context).shareContent = event.content;
|
||||
} else if (selectedEvents.length == 1) {
|
||||
Matrix.of(context).shareContent = selectedEvents.first.content;
|
||||
} else {
|
||||
Matrix.of(context).shareContent = {
|
||||
|
@ -343,9 +346,9 @@ class _ChatState extends State<_Chat> {
|
|||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
void replyAction() {
|
||||
void replyAction({Event replyTo}) {
|
||||
setState(() {
|
||||
replyEvent = selectedEvents.first;
|
||||
replyEvent = replyTo ?? selectedEvents.first;
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
|
@ -411,6 +414,128 @@ class _ChatState extends State<_Chat> {
|
|||
e.type != 'm.reaction')
|
||||
.toList();
|
||||
|
||||
SwipeDirection _getSwipeDirection(Event event) {
|
||||
var swipeToEndAction = Matrix.of(context).swipeToEndAction;
|
||||
var swipeToStartAction = Matrix.of(context).swipeToStartAction;
|
||||
var client = Matrix.of(context).client;
|
||||
if (event.senderId != client.userID && swipeToEndAction == 'edit') {
|
||||
swipeToEndAction = null;
|
||||
}
|
||||
if (event.senderId != client.userID && swipeToStartAction == 'edit') {
|
||||
swipeToStartAction = null;
|
||||
}
|
||||
if (swipeToEndAction != null && swipeToStartAction != null) {
|
||||
return SwipeDirection.horizontal;
|
||||
}
|
||||
if (swipeToEndAction != null) {
|
||||
return SwipeDirection.startToEnd;
|
||||
}
|
||||
if (swipeToStartAction != null) {
|
||||
return SwipeDirection.endToStart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _getSwipeBackground(Event event, {bool isSecondary = false}) {
|
||||
var alignToRight, action;
|
||||
if (_getSwipeDirection(event) == SwipeDirection.horizontal) {
|
||||
if (isSecondary) {
|
||||
alignToRight = true;
|
||||
action = Matrix.of(context).swipeToStartAction;
|
||||
} else {
|
||||
alignToRight = false;
|
||||
action = Matrix.of(context).swipeToEndAction;
|
||||
}
|
||||
} else if (isSecondary) {
|
||||
return null;
|
||||
} else if (_getSwipeDirection(event) == SwipeDirection.endToStart) {
|
||||
alignToRight = true;
|
||||
action = Matrix.of(context).swipeToStartAction;
|
||||
} else {
|
||||
alignToRight = false;
|
||||
action = Matrix.of(context).swipeToStartAction;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'reply':
|
||||
return Container(
|
||||
color: Theme.of(context).primaryColor.withAlpha(100),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.reply_outlined),
|
||||
SizedBox(width: 2.0),
|
||||
Text(L10n.of(context).reply)
|
||||
],
|
||||
),
|
||||
);
|
||||
case 'forward':
|
||||
return Container(
|
||||
color: Theme.of(context).primaryColor.withAlpha(100),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.forward_outlined),
|
||||
SizedBox(width: 2.0),
|
||||
Text(L10n.of(context).forward)
|
||||
],
|
||||
),
|
||||
);
|
||||
case 'edit':
|
||||
return Container(
|
||||
color: Theme.of(context).primaryColor.withAlpha(100),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.edit_outlined),
|
||||
SizedBox(width: 2.0),
|
||||
Text(L10n.of(context).edit)
|
||||
],
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
color: Theme.of(context).primaryColor.withAlpha(100),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipe(SwipeDirection direction, Event event) {
|
||||
var action;
|
||||
if (direction == SwipeDirection.endToStart) {
|
||||
action = Matrix.of(context).swipeToStartAction;
|
||||
} else {
|
||||
action = Matrix.of(context).swipeToEndAction;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'reply':
|
||||
replyAction(replyTo: event);
|
||||
break;
|
||||
case 'forward':
|
||||
forwardEventsAction(context, event: event);
|
||||
break;
|
||||
case 'edit':
|
||||
setState(() {
|
||||
editEvent = event;
|
||||
sendController.text = editEvent
|
||||
.getDisplayEvent(timeline)
|
||||
.getLocalizedBody(MatrixLocals(L10n.of(context)),
|
||||
withSenderNamePrefix: false, hideReply: true);
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
matrix = Matrix.of(context);
|
||||
|
@ -668,43 +793,57 @@ class _ChatState extends State<_Chat> {
|
|||
key: ValueKey(i - 1),
|
||||
index: i - 1,
|
||||
controller: _scrollController,
|
||||
child: Message(filteredEvents[i - 1],
|
||||
onAvatarTab: (Event event) {
|
||||
sendController.text +=
|
||||
' ${event.senderId}';
|
||||
},
|
||||
onSelect: (Event event) {
|
||||
if (!event.redacted) {
|
||||
if (selectedEvents
|
||||
.contains(event)) {
|
||||
setState(
|
||||
() => selectedEvents
|
||||
.remove(event),
|
||||
);
|
||||
} else {
|
||||
setState(
|
||||
() =>
|
||||
selectedEvents.add(event),
|
||||
child: Swipeable(
|
||||
key: ValueKey(
|
||||
filteredEvents[i - 1].eventId),
|
||||
background: _getSwipeBackground(
|
||||
filteredEvents[i - 1]),
|
||||
secondaryBackground:
|
||||
_getSwipeBackground(
|
||||
filteredEvents[i - 1],
|
||||
isSecondary: true),
|
||||
direction: _getSwipeDirection(
|
||||
filteredEvents[i - 1]),
|
||||
onSwipe: (direction) => _handleSwipe(
|
||||
direction, filteredEvents[i - 1]),
|
||||
child: Message(filteredEvents[i - 1],
|
||||
onAvatarTab: (Event event) {
|
||||
sendController.text +=
|
||||
' ${event.senderId}';
|
||||
},
|
||||
onSelect: (Event event) {
|
||||
if (!event.redacted) {
|
||||
if (selectedEvents
|
||||
.contains(event)) {
|
||||
setState(
|
||||
() => selectedEvents
|
||||
.remove(event),
|
||||
);
|
||||
} else {
|
||||
setState(
|
||||
() => selectedEvents
|
||||
.add(event),
|
||||
);
|
||||
}
|
||||
selectedEvents.sort(
|
||||
(a, b) => a.originServerTs
|
||||
.compareTo(
|
||||
b.originServerTs),
|
||||
);
|
||||
}
|
||||
selectedEvents.sort(
|
||||
(a, b) => a.originServerTs
|
||||
.compareTo(
|
||||
b.originServerTs),
|
||||
);
|
||||
}
|
||||
},
|
||||
scrollToEventId: (String eventId) =>
|
||||
_scrollToEventId(eventId,
|
||||
context: context),
|
||||
longPressSelect:
|
||||
selectedEvents.isEmpty,
|
||||
selected: selectedEvents
|
||||
.contains(filteredEvents[i - 1]),
|
||||
timeline: timeline,
|
||||
nextEvent: i >= 2
|
||||
? filteredEvents[i - 2]
|
||||
: null),
|
||||
},
|
||||
scrollToEventId: (String eventId) =>
|
||||
_scrollToEventId(eventId,
|
||||
context: context),
|
||||
longPressSelect:
|
||||
selectedEvents.isEmpty,
|
||||
selected: selectedEvents.contains(
|
||||
filteredEvents[i - 1]),
|
||||
timeline: timeline,
|
||||
nextEvent: i >= 2
|
||||
? filteredEvents[i - 2]
|
||||
: null),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -171,7 +171,6 @@ class _LoginState extends State<Login> {
|
|||
readOnly: loading,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (t) => _checkWellKnownWithCoolDown(t, context),
|
||||
controller: usernameController,
|
||||
decoration: InputDecoration(
|
||||
|
|
|
@ -22,6 +22,74 @@ class ChatSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatSettingsState extends State<ChatSettings> {
|
||||
String _getActionDescription(String action) {
|
||||
switch (action) {
|
||||
case 'reply':
|
||||
return L10n.of(context).reply;
|
||||
case 'forward':
|
||||
return L10n.of(context).forward;
|
||||
case 'edit':
|
||||
return L10n.of(context).edit;
|
||||
default:
|
||||
return L10n.of(context).none;
|
||||
}
|
||||
}
|
||||
|
||||
void _changeSwipeAction(bool isToEnd, String action) async {
|
||||
if (isToEnd) {
|
||||
Matrix.of(context).swipeToEndAction = action;
|
||||
await Matrix.of(context)
|
||||
.store
|
||||
.setItem('chat.fluffy.swipeToEndAction', action);
|
||||
setState(() => null);
|
||||
} else {
|
||||
Matrix.of(context).swipeToStartAction = action;
|
||||
await Matrix.of(context)
|
||||
.store
|
||||
.setItem('chat.fluffy.swipeToStartAction', action);
|
||||
setState(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _swipeActionChooser(BuildContext context, bool isToEnd) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).none),
|
||||
leading: Icon(Icons.clear_outlined),
|
||||
onTap: () {
|
||||
_changeSwipeAction(isToEnd, null);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).reply),
|
||||
leading: Icon(Icons.reply_outlined),
|
||||
onTap: () {
|
||||
_changeSwipeAction(isToEnd, 'reply');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).forward),
|
||||
leading: Icon(Icons.forward_outlined),
|
||||
onTap: () {
|
||||
_changeSwipeAction(isToEnd, 'forward');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).edit),
|
||||
leading: Icon(Icons.edit_outlined),
|
||||
onTap: () {
|
||||
_changeSwipeAction(isToEnd, 'edit');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -42,6 +110,27 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
},
|
||||
),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).swipeToEndAction),
|
||||
onTap: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
_swipeActionChooser(context, true),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getActionDescription(Matrix.of(context).swipeToEndAction)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).swipeToStartAction),
|
||||
onTap: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
_swipeActionChooser(context, false),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getActionDescription(Matrix.of(context).swipeToStartAction)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -553,7 +553,7 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: bd24832f96537447174aa34ba78eaed7ff05bb8e
|
||||
resolved-ref: c8eb59c25c4e3a568bd64e4722108ec45259e157
|
||||
url: "https://gitlab.com/famedly/libraries/native_imaging.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
@ -870,6 +870,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.1"
|
||||
swipe_to_action:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: swipe_to_action
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -69,6 +69,7 @@ dependencies:
|
|||
ref: master
|
||||
flutter_blurhash: ^0.5.0
|
||||
scroll_to_index: ^1.0.6
|
||||
swipe_to_action: ^0.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
59
snap/snapcraft.yaml
Normal file
59
snap/snapcraft.yaml
Normal file
|
@ -0,0 +1,59 @@
|
|||
name: fluffychat
|
||||
base: core18 # the base snap is the execution environment for this snap
|
||||
version: git # just for humans, typically '1.2+git' or '1.3.2'
|
||||
summary: Open. Nonprofit. Cute ♥
|
||||
description: |
|
||||
FluffyChat - Chat with your friends
|
||||
|
||||
9 greatest FluffyChat features:
|
||||
1. Opensource and open development where everyone can join.
|
||||
2. Nonprofit - FluffyChat is donation funded.
|
||||
3. Cute design and many theme settings including a dark mode.
|
||||
4. Unlimited groups and direct chats.
|
||||
5. FluffyChat is made as simple to use as possible.
|
||||
6. Free to use for everyone without ads.
|
||||
7. FluffyChat can use your addressbook to find your friends or you can use
|
||||
usernames.
|
||||
8. There is no "FluffyChat server" you are forced to use. Use the server
|
||||
you find trustworthy or host your own.
|
||||
9. Compatible with Riot, Fractal, Nekho and all matrix messengers.
|
||||
|
||||
Join the community: fluffychat://+ubports_community:matrix.org
|
||||
Website: http://fluffy.chat
|
||||
Microblog: https://metalhead.club/@krille
|
||||
|
||||
grade: devel # must be 'stable' to release into candidate/stable channels
|
||||
confinement: strict # use 'strict' once you have the right plugs and slots
|
||||
|
||||
parts:
|
||||
olm:
|
||||
plugin: cmake
|
||||
source: https://gitlab.matrix.org/matrix-org/olm.git
|
||||
source-type: git
|
||||
source-tag: 3.2.1
|
||||
fluffychat:
|
||||
plugin: flutter
|
||||
source: .
|
||||
flutter-target: lib/main.dart
|
||||
stage-packages:
|
||||
- libsqlite3-0
|
||||
override-prime: |
|
||||
snapcraftctl prime
|
||||
ln -sf libsqlite3.so.0 ${SNAPCRAFT_PRIME}/usr/lib/x86_64-linux-gnu/libsqlite3.so
|
||||
|
||||
slots:
|
||||
dbus-svc:
|
||||
interface: dbus
|
||||
bus: session
|
||||
name: chat.fluffy.fluffychat
|
||||
|
||||
apps:
|
||||
fluffychat:
|
||||
command: fluffychat
|
||||
extensions:
|
||||
- flutter-dev
|
||||
plugs:
|
||||
- network
|
||||
- home
|
||||
slots:
|
||||
- dbus-svc
|
Loading…
Reference in a new issue