Merge pull request #8 from innereq/swipe-to-reply

Swipe to reply
This commit is contained in:
Inex Code 2020-10-14 06:43:22 +03:00 committed by GitHub
commit 326f100520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1035 additions and 315 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@
.svn/ .svn/
lib/generated_plugin_registrant.dart lib/generated_plugin_registrant.dart
google-services.json google-services.json
prime
# libolm package # libolm package
/assets/js/package/* /assets/js/package/*

View File

@ -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" - 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 - 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" - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && fdroid update"
dependencies: needs: ["build_android_apk"]
- build_android_apk
only: only:
- tags - tags
@ -174,8 +173,7 @@ pages:
- cd build/web/ && bundle install && cd ../../ - cd build/web/ && bundle install && cd ../../
- cd build/web/ && bundle exec jekyll build -d public && cd ../../ - cd build/web/ && bundle exec jekyll build -d public && cd ../../
- mv build/web/public ./ - mv build/web/public ./
dependencies: needs: ["build_web"]
- build_web
artifacts: artifacts:
paths: paths:
- public - public
@ -215,3 +213,4 @@ snap:publish:
- './*.snap' - './*.snap'
when: on_success when: on_success
expire_in: 1 week expire_in: 1 week
needs: []

View File

@ -3,10 +3,12 @@
An experimental fork of FluffyChat. An experimental fork of FluffyChat.
# Changes from FluffyChat # Changes from FluffyChat
* Swipe to reply (or forward/edit)
* Reworked auth flow * Reworked auth flow
* Removed Sentry * Removed Sentry
* Double check of .well-known * Double check of .well-known
* Get Jitsi instance from .well-known * Get Jitsi instance from .well-known
* Redesigned settings
# Features # Features
* Single and group chats * Single and group chats
@ -35,8 +37,8 @@ An experimental fork of FluffyChat.
2. Clone the repo: 2. Clone the repo:
``` ```
git clone --recurse-submodules https://gitlab.com/ChristianPauly/fluffychat-flutter git clone --recurse-submodules https://github.com/innereq/FurryChat.git
cd fluffychat-flutter cd FurryChat
``` ```
3. Choose your target platform below and enable support for it. 3. Choose your target platform below and enable support for it.
@ -80,13 +82,6 @@ flutter build windows --release
flutter build macos --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 ## How to add translations for your language
You can use Weblate to translate the app to your language: You can use Weblate to translate the app to your language:

View File

@ -1,4 +1,5 @@
arb-dir: lib/l10n arb-dir: lib/l10n
template-arb-file: intl_en.arb template-arb-file: intl_en.arb
output-localization-file: l10n.dart output-localization-file: l10n.dart
output-class: L10n output-class: L10n
preferred-supported-locales: ["en"]

View File

@ -11,7 +11,6 @@ import 'package:furrychat/utils/user_status.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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:universal_html/prefer_universal/html.dart' as html;
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -72,13 +71,15 @@ class MatrixState extends State<Matrix> {
File wallpaper; File wallpaper;
bool renderHtml = false; bool renderHtml = false;
String swipeToEndAction;
String swipeToStartAction = 'reply';
String jitsiInstance = 'https://meet.jit.si/'; String jitsiInstance = 'https://meet.jit.si/';
void clean() async { void clean() async {
if (!kIsWeb) return; if (!kIsWeb) return;
final storage = LocalStorage('LocalStorage'); final storage = await getLocalStorage();
await storage.ready;
await storage.deleteItem(widget.clientName); await storage.deleteItem(widget.clientName);
} }
@ -285,6 +286,16 @@ class MatrixState extends State<Matrix> {
store.getItem('chat.fluffy.renderHtml').then((final render) async { store.getItem('chat.fluffy.renderHtml').then((final render) async {
renderHtml = render == '1'; 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) { if (kIsWeb) {
onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true); onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true);

View File

@ -189,7 +189,7 @@
"username": {} "username": {}
} }
}, },
"changedTheDisplaynameTo": "{username} غيّر اسمه الى {displayname}", "changedTheDisplaynameTo": "{username} غيّر اسمه العلني الى {displayname}",
"@changedTheDisplaynameTo": { "@changedTheDisplaynameTo": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -375,7 +375,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"couldNotSetDisplayname": "تعذر تعيين الاسم", "couldNotSetDisplayname": "تعذر تعيين الاسم العلني",
"@couldNotSetDisplayname": { "@couldNotSetDisplayname": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -489,7 +489,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"displaynameHasBeenChanged": "غُيِّر الاسم", "displaynameHasBeenChanged": "غُيِّر الاسم العلني",
"@displaynameHasBeenChanged": { "@displaynameHasBeenChanged": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -499,7 +499,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"editDisplayname": "حرر الاسم", "editDisplayname": "حرر الاسم العلني",
"@editDisplayname": { "@editDisplayname": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -514,7 +514,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة", "emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة!",
"@emoteWarnNeedToPick": { "@emoteWarnNeedToPick": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -1374,7 +1374,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"useAmoledTheme": "", "useAmoledTheme": "هل تريد استخدم ألوان متوافقة مع Amoled؟",
"@useAmoledTheme": { "@useAmoledTheme": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
@ -1507,14 +1507,14 @@
"unreadCount": {} "unreadCount": {}
} }
}, },
"unreadMessages": "", "unreadMessages": "{unreadEvents} رسالة غير مقروءة",
"@unreadMessages": { "@unreadMessages": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
"unreadEvents": {} "unreadEvents": {}
} }
}, },
"unreadMessagesInChats": "", "unreadMessagesInChats": "{unreadEvents} رسالة غير مقروءة من {unreadChats} محادثة",
"@unreadMessagesInChats": { "@unreadMessagesInChats": {
"type": "text", "type": "text",
"placeholders": { "placeholders": {
@ -1702,5 +1702,10 @@
"@yourOwnUsername": { "@yourOwnUsername": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
},
"privacy": "الخصوصية",
"@privacy": {
"type": "text",
"placeholders": {}
} }
} }

View File

@ -514,6 +514,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"edit": "Edit",
"@edit": {
"type": "text",
"placeholders": {}
},
"editDisplayname": "Edit displayname", "editDisplayname": "Edit displayname",
"@editDisplayname": { "@editDisplayname": {
"type": "text", "type": "text",
@ -1439,6 +1444,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"swipeToEndAction": "Swipe to right action",
"@swipeToEndAction": {
"type": "text",
"placeholders": {}
},
"swipeToStartAction": "Swipe to left action",
"@swipeToStartAction": {
"type": "text",
"placeholders": {}
},
"donate": "Donate", "donate": "Donate",
"@donate": { "@donate": {
"type": "text", "type": "text",

View File

@ -194,5 +194,272 @@
"@about": { "@about": {
"type": "text", "type": "text",
"placeholders": {} "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": {}
} }
} }

View File

@ -114,6 +114,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"avatar": "Аватар",
"@avatar": {
"type": "text",
"placeholders": {}
},
"avatarHasBeenChanged": "Аватар был изменён", "avatarHasBeenChanged": "Аватар был изменён",
"@avatarHasBeenChanged": { "@avatarHasBeenChanged": {
"type": "text", "type": "text",
@ -197,6 +202,11 @@
"displayname": {} "displayname": {}
} }
}, },
"changeThePassword": "Сменить пароль",
"@changeThePassword": {
"type": "text",
"placeholders": {}
},
"changedTheGuestAccessRules": "{username} изменил(а) правила гостевого доступа", "changedTheGuestAccessRules": "{username} изменил(а) правила гостевого доступа",
"@changedTheGuestAccessRules": { "@changedTheGuestAccessRules": {
"type": "text", "type": "text",
@ -509,6 +519,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"edit": "Редактировать",
"@edit": {
"type": "text",
"placeholders": {}
},
"editDisplayname": "Отображаемое имя", "editDisplayname": "Отображаемое имя",
"@editDisplayname": { "@editDisplayname": {
"type": "text", "type": "text",
@ -681,11 +696,21 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"homeserver": "Сервер Matrix",
"@homeserver": {
"type": "text",
"placeholders": {}
},
"homeserverIsNotCompatible": "Несовместимый сервер Matrix", "homeserverIsNotCompatible": "Несовместимый сервер Matrix",
"@homeserverIsNotCompatible": { "@homeserverIsNotCompatible": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"homeserverOrMXID": "Сервер или полное имя пользователя",
"@homeserverOrMXID": {
"type": "text",
"placeholders": {}
},
"id": "ID", "id": "ID",
"@id": { "@id": {
"type": "text", "type": "text",
@ -1372,6 +1397,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"swipeToEndAction": "Действие по жесту вправо",
"@swipeToEndAction": {
"type": "text",
"placeholders": {}
},
"swipeToStartAction": "Действие по жесту влево",
"@swipeToStartAction": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Системная", "systemTheme": "Системная",
"@systemTheme": { "@systemTheme": {
"type": "text", "type": "text",

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.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:universal_html/prefer_universal/html.dart' as html;
import 'components/matrix.dart'; import 'components/matrix.dart';
@ -21,8 +20,6 @@ void main() {
runZonedGuarded( runZonedGuarded(
() => runApp(App()), () => runApp(App()),
(error, stackTrace) async { (error, stackTrace) async {
final storage = LocalStorage('LocalStorage');
await storage.ready;
debugPrint(error.toString()); debugPrint(error.toString());
debugPrint(stackTrace.toString()); debugPrint(stackTrace.toString());
}, },

View File

@ -6,12 +6,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:localstorage/localstorage.dart'; import 'package:localstorage/localstorage.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:async'; import 'dart:async';
import 'dart:core'; import 'dart:core';
import './database/shared.dart'; import './database/shared.dart';
import 'package:olm/olm.dart' as olm; // needed for migration import 'package:olm/olm.dart' as olm; // needed for migration
import 'package:random_string/random_string.dart'; 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 { Future<Database> getDatabase(Client client) async {
while (_generateDatabaseLock) { while (_generateDatabaseLock) {
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(Duration(milliseconds: 50));

View File

@ -18,8 +18,8 @@ Future<MatrixImageFile> resizeImage(MatrixImageFile file,
try { try {
final nativeImg = native.Image(); final nativeImg = native.Image();
await nativeImg.loadEncoded(file.bytes); await nativeImg.loadEncoded(file.bytes);
file.width = nativeImg.width(); file.width = nativeImg.width;
file.height = nativeImg.height(); file.height = nativeImg.height;
args = _IsolateArgs( args = _IsolateArgs(
width: file.width, height: file.height, bytes: file.bytes, max: max); width: file.width, height: file.height, bytes: file.bytes, max: max);
nativeImg.free(); nativeImg.free();
@ -96,8 +96,8 @@ Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async {
final ret = _IsolateResponse( final ret = _IsolateResponse(
blurhash: blurhash, blurhash: blurhash,
jpegBytes: jpegBytes, jpegBytes: jpegBytes,
width: nativeImg.width(), width: nativeImg.width,
height: nativeImg.height()); height: nativeImg.height);
nativeImg.free(); nativeImg.free();

View File

@ -28,6 +28,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:scroll_to_index/scroll_to_index.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/dialogs/send_file_dialog.dart';
import '../components/input_bar.dart'; import '../components/input_bar.dart';
@ -316,8 +317,10 @@ class _ChatState extends State<_Chat> {
return true; return true;
} }
void forwardEventsAction(BuildContext context) async { void forwardEventsAction(BuildContext context, {Event event}) async {
if (selectedEvents.length == 1) { if (event != null) {
Matrix.of(context).shareContent = event.content;
} else if (selectedEvents.length == 1) {
Matrix.of(context).shareContent = selectedEvents.first.content; Matrix.of(context).shareContent = selectedEvents.first.content;
} else { } else {
Matrix.of(context).shareContent = { Matrix.of(context).shareContent = {
@ -343,9 +346,9 @@ class _ChatState extends State<_Chat> {
setState(() => selectedEvents.clear()); setState(() => selectedEvents.clear());
} }
void replyAction() { void replyAction({Event replyTo}) {
setState(() { setState(() {
replyEvent = selectedEvents.first; replyEvent = replyTo ?? selectedEvents.first;
selectedEvents.clear(); selectedEvents.clear();
}); });
inputFocus.requestFocus(); inputFocus.requestFocus();
@ -411,6 +414,128 @@ class _ChatState extends State<_Chat> {
e.type != 'm.reaction') e.type != 'm.reaction')
.toList(); .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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
matrix = Matrix.of(context); matrix = Matrix.of(context);
@ -668,43 +793,57 @@ class _ChatState extends State<_Chat> {
key: ValueKey(i - 1), key: ValueKey(i - 1),
index: i - 1, index: i - 1,
controller: _scrollController, controller: _scrollController,
child: Message(filteredEvents[i - 1], child: Swipeable(
onAvatarTab: (Event event) { key: ValueKey(
sendController.text += filteredEvents[i - 1].eventId),
' ${event.senderId}'; background: _getSwipeBackground(
}, filteredEvents[i - 1]),
onSelect: (Event event) { secondaryBackground:
if (!event.redacted) { _getSwipeBackground(
if (selectedEvents filteredEvents[i - 1],
.contains(event)) { isSecondary: true),
setState( direction: _getSwipeDirection(
() => selectedEvents filteredEvents[i - 1]),
.remove(event), onSwipe: (direction) => _handleSwipe(
); direction, filteredEvents[i - 1]),
} else { child: Message(filteredEvents[i - 1],
setState( onAvatarTab: (Event event) {
() => sendController.text +=
selectedEvents.add(event), ' ${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 scrollToEventId: (String eventId) =>
.compareTo( _scrollToEventId(eventId,
b.originServerTs), context: context),
); longPressSelect:
} selectedEvents.isEmpty,
}, selected: selectedEvents.contains(
scrollToEventId: (String eventId) => filteredEvents[i - 1]),
_scrollToEventId(eventId, timeline: timeline,
context: context), nextEvent: i >= 2
longPressSelect: ? filteredEvents[i - 2]
selectedEvents.isEmpty, : null),
selected: selectedEvents ),
.contains(filteredEvents[i - 1]),
timeline: timeline,
nextEvent: i >= 2
? filteredEvents[i - 2]
: null),
); );
}); });
}, },

View File

@ -171,7 +171,6 @@ class _LoginState extends State<Login> {
readOnly: loading, readOnly: loading,
autocorrect: false, autocorrect: false,
autofocus: true, autofocus: true,
keyboardType: TextInputType.emailAddress,
onChanged: (t) => _checkWellKnownWithCoolDown(t, context), onChanged: (t) => _checkWellKnownWithCoolDown(t, context),
controller: usernameController, controller: usernameController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@ -22,6 +22,74 @@ class ChatSettings extends StatefulWidget {
} }
class _ChatSettingsState extends State<ChatSettings> { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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)),
),
], ],
), ),
); );

View File

@ -553,7 +553,7 @@ packages:
description: description:
path: "." path: "."
ref: master ref: master
resolved-ref: bd24832f96537447174aa34ba78eaed7ff05bb8e resolved-ref: c8eb59c25c4e3a568bd64e4722108ec45259e157
url: "https://gitlab.com/famedly/libraries/native_imaging.git" url: "https://gitlab.com/famedly/libraries/native_imaging.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -870,6 +870,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety.1" 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: synchronized:
dependency: transitive dependency: transitive
description: description:

View File

@ -69,6 +69,7 @@ dependencies:
ref: master ref: master
flutter_blurhash: ^0.5.0 flutter_blurhash: ^0.5.0
scroll_to_index: ^1.0.6 scroll_to_index: ^1.0.6
swipe_to_action: ^0.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

59
snap/snapcraft.yaml Normal file
View 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