Browse Source

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

Swipe to reply
yiffed
Inex Code 8 months ago
committed by GitHub
parent
commit
326f100520
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 7
      .gitlab-ci.yml
  3. 13
      README.md
  4. 3
      l10n.yaml
  5. 17
      lib/components/matrix.dart
  6. 21
      lib/l10n/intl_ar.arb
  7. 15
      lib/l10n/intl_en.arb
  8. 267
      lib/l10n/intl_it.arb
  9. 35
      lib/l10n/intl_ru.arb
  10. 574
      lib/l10n/intl_zh.arb
  11. 3
      lib/main.dart
  12. 10
      lib/utils/famedlysdk_store.dart
  13. 8
      lib/utils/resize_image.dart
  14. 217
      lib/views/chat.dart
  15. 1
      lib/views/login.dart
  16. 89
      lib/views/settings/settings_chat.dart
  17. 9
      pubspec.lock
  18. 1
      pubspec.yaml
  19. 59
      snap/snapcraft.yaml

1
.gitignore

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

7
.gitlab-ci.yml

@ -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

@ -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:

3
l10n.yaml

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

17
lib/components/matrix.dart

@ -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);

21
lib/l10n/intl_ar.arb

@ -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": {}
}
}

15
lib/l10n/intl_en.arb

@ -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",

267
lib/l10n/intl_it.arb

@ -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": {}
}
}

35
lib/l10n/intl_ru.arb

@ -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",

574
lib/l10n/intl_zh.arb
File diff suppressed because it is too large
View File

3
lib/main.dart

@ -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());
},

10
lib/utils/famedlysdk_store.dart

@ -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));

8
lib/utils/resize_image.dart

@ -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();

217
lib/views/chat.dart

@ -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),
),
);
});
},

1
lib/views/login.dart

@ -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(

89
lib/views/settings/settings_chat.dart

@ -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)),
),
],
),
);

9
pubspec.lock

@ -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:

1
pubspec.yaml

@ -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

@ -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…
Cancel
Save